Go and ElasticSearch full-text search microservice in k8s👋✨💫
Tags: go, elasticsearch, rabbitmq, kubernetes
Published: 2022-08-16 12:05:10

#### 👨‍💻 Full list what has been used:
[Elasticsearch](https://github.com/elastic/go-elasticsearch) client for Go 
[RabbitMQ](https://github.com/rabbitmq/amqp091-go) Go RabbitMQ Client Library 
[Jaeger](https://www.jaegertracing.io/) open source, end-to-end distributed [tracing](https://opentracing.io/) 
[Prometheus](https://prometheus.io/) monitoring and alerting 
[Grafana](https://grafana.com/) for to compose observability dashboards with everything from Prometheus 
[Echo](https://github.com/labstack/echo) web framework 
[Kibana](https://github.com/labstack/echo) is user interface that lets you visualize your Elasticsearch 
[Docker](https://www.docker.com/) and docker-compose 
[Kubernetes](https://kubernetes.io/) K8s 
[Helm](https://helm.sh/) The package manager for Kubernetes 

Source code you can find in [GitHub repository](https://github.com/AleksK1NG/Go-elasticsearch-rabbitmq).
The main idea of this project is the implementation of a full-text search with support for synonyms, mistyping, and the wrong keyboard layout
using [Elasticsearch](https://github.com/elastic/go-elasticsearch) and [RabbitMQ](https://github.com/rabbitmq/amqp091-go).

All UI interfaces will be available on ports:

#### Grafana UI: http://localhost:3005
<img src="https://i.postimg.cc/bN9kJQzW/Go-Processes-Grafana-2022-08-14-15-01-16.png" alt="Grafana"/>

#### Kibana UI: http://localhost:5601/app/home#/
<img src="https://i.postimg.cc/gk84KXGH/Console-Dev-Tools-Elastic-2022-08-14-14-59-58.png" alt="Kibana"/>

#### RabbitMQ UI: http://localhost:15672
<img src="https://i.postimg.cc/sgk50jqW/Rabbit-MQ-Management-2022-08-14-13-50-20.png" alt="RabbitMQ"/>

#### Jaeger UI: http://localhost:16686
<img src="https://i.postimg.cc/9QRn7fyw/Jaeger-UI-2022-08-14-13-46-39.png" alt="Jaeger"/>

#### Prometheus UI: http://localhost:9090
<img src="https://i.postimg.cc/wTHNN4JX/Prometheus-Time-Series-Collection-and-Processing-Server-2022-08-14-13-40-47.png" alt="Prometheus"/>


Docker-compose file for this project:
```yaml
version: "3.9"

services:
  rabbitmq:
    image: rabbitmq:3.9-management-alpine
    container_name: rabbitmq
    restart: always
    ports:
      - "5672:5672"
      - "15672:15672"
    networks: [ "microservices" ]

  node01:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.3.3
    container_name: node01
    restart: always
    environment:
      - node.name=node01
      - cluster.name=es-cluster-8
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - xpack.license.self_generated.type=basic
      - xpack.security.enabled=false
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - ./es-data01:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
      - "9300:9300"
    networks: [ "microservices" ]

  kibana:
    image: docker.elastic.co/kibana/kibana:8.3.3
    restart: always
    environment:
      ELASTICSEARCH_HOSTS: http://node01:9200
    ports:
      - "5601:5601"
    depends_on:
      - node01
    networks: [ "microservices" ]

  jaeger:
    container_name: jaeger_container
    restart: always
    image: jaegertracing/all-in-one:1.35
    environment:
      - COLLECTOR_ZIPKIN_HTTP_PORT=9411
    ports:
      - "5775:5775/udp"
      - "6831:6831/udp"
      - "6832:6832/udp"
      - "5778:5778"
      - "16686:16686"
      - "14268:14268"
      - "14250:14250"
      - "9411:9411"
    networks: [ "microservices" ]

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    command:
      - --config.file=/etc/prometheus/prometheus.yml
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    networks: [ "microservices" ]

  node_exporter:
    container_name: node_exporter_container
    restart: always
    image: prom/node-exporter
    ports:
      - '9101:9100'
    networks: [ "microservices" ]

  grafana:
    container_name: grafana_container
    restart: always
    image: grafana/grafana
    ports:
      - '3005:3000'
    networks: [ "microservices" ]

volumes:
  es-data01:
    driver: local

networks:
  microservices:
    name: microservices
```

Full-text search with auto-completion can be realized in different ways, it's up to you choose which is better for your case.
First, we have to create mappings for our index, of course, elasticsearch can create it for us, but it's not a production solution.
As data model used here simple abstract shopping product item with searchable title, description, and shop name fields, for this example it's enough.
The creation of the right mapping is very important and tricky,
to realize full-text search with synonyms, mistyping, and wrong keyboard layout we will configure our own analyzer,
which combines the character filters, tokenizer, and token filters.

Our **ngram_filter** is elastic builtin [edge_ngram](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-edgengram-tokenfilter.html) filter,
for understand how it works highly recommend read elasticsearch documentation,
We have to assign min and max values, it always depends on each unique case.
Next, let's specify **name_synonym_filter**, in **synonyms** field we have to add an array of strings, each string is a comma separate synonyms which will be used for search.

<img src="https://i.postimg.cc/ncw2vpxY/Console-Dev-Tools-Elastic-2022-08-15-13-50-17.png" alt="analyzer"/>

In mapping properties let's create our document fields mappings, for full-text search field is **"type": "text"**,
as an analyzer our newly created **autocomplete_analyzer**, it's used when elastic index documents, and for search queries, we don't need so complex analyzer it will only slow down our search queries,
so we add another one **search_analyzer**: **standard**
Handling of wrong keyboard language layout we can implement in different ways, on elasticsearch side by synonyms, but it makes mappings to huge
and on the application level by creating a mapping for keyboard language then map users search term to apposite language and send to elastic query for search any one of it,
so for example search the words _"Apple, apple, яблоко, Яблоко, z,kjrj, Z,kjrj, Фззду, фззду"_ will find the same for the all of it.
In real-world scenarios usually mapping is much more complicated but here for example it's enough:

<img src="https://i.postimg.cc/59QLZLWv/Console-Dev-Tools-Elastic-2022-08-15-13-51-12.png" alt="mappings"/>

For **Go** available two good libraries for elasticsearch, the official [Elasticsearch](https://github.com/elastic/go-elasticsearch) client and another one from community [olivere elastic](https://github.com/olivere/elastic),
both is good, but at this moment only the official client supports 8 version of elasticsearch and for serious production think it's the choice.

Our microservice interacts by HTTP using [Echo](https://github.com/labstack/echo) web framework and [RabbitMQ](https://github.com/rabbitmq/amqp091-go) official client,
REST controller has index document and search methods:

```go
func (h *productController) indexAsync() echo.HandlerFunc {
	return func(c echo.Context) error {
		ctx, span := tracing.StartHttpServerTracerSpan(c, "productController.indexAsync")
		defer span.Finish()

		var product domain.Product
		if err := c.Bind(&product); err != nil {
			h.log.Errorf("(Bind) err: %v", tracing.TraceWithErr(span, err))
			return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse)
		}
		product.ID = uuid.NewV4().String()

		if err := h.productUseCase.IndexAsync(ctx, product); err != nil {
			h.log.Errorf("(productUseCase.IndexAsync) err: %v", tracing.TraceWithErr(span, err))
			return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse)
		}

		h.log.Infof("created product: %+v", product)
		h.metrics.HttpSuccessIndexAsyncRequests.Inc()
		return c.JSON(http.StatusCreated, product)
	}
}

func (h *productController) index() echo.HandlerFunc {
	return func(c echo.Context) error {
		ctx, span := tracing.StartHttpServerTracerSpan(c, "productController.index")
		defer span.Finish()

		var product domain.Product
		if err := c.Bind(&product); err != nil {
			h.log.Errorf("(Bind) err: %v", tracing.TraceWithErr(span, err))
			return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse)
		}
		product.ID = uuid.NewV4().String()

		if err := h.productUseCase.Index(ctx, product); err != nil {
			h.log.Errorf("(productUseCase.Index) err: %v", tracing.TraceWithErr(span, err))
			return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse)
		}

		h.log.Infof("created product: %+v", product)
		h.metrics.HttpSuccessIndexRequests.Inc()
		return c.JSON(http.StatusCreated, product)
	}
}

func (h *productController) search() echo.HandlerFunc {
	return func(c echo.Context) error {
		ctx, span := tracing.StartHttpServerTracerSpan(c, "productController.search")
		defer span.Finish()

		searchTerm := c.QueryParam("search")
		pagination := utils.NewPaginationFromQueryParams(c.QueryParam(constants.Size), c.QueryParam(constants.Page))

		searchResult, err := h.productUseCase.Search(ctx, searchTerm, pagination)
		if err != nil {
			h.log.Errorf("(productUseCase.Search) err: %v", tracing.TraceWithErr(span, err))
			return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse)
		}

		h.log.Infof("search result: %s", searchResult.PaginationResponse.String())
		h.metrics.HttpSuccessSearchRequests.Inc()
		span.LogFields(log.String("search result", searchResult.PaginationResponse.String()))
		return c.JSON(http.StatusOK, dto.SearchProductsResponse{
			SearchTerm: searchTerm,
			Pagination: searchResult.PaginationResponse,
			Products:   searchResult.List,
		})
	}
}
```

<img src="https://i.postimg.cc/QtLW7NXJ/Postman-2022-08-15-11-55-23.png" alt="Postman"/>

The useCase for IndexAsync method serialize data and publish it to RabbitMQ:

```go
func (p *productUseCase) IndexAsync(ctx context.Context, product domain.Product) error {
	span, ctx := opentracing.StartSpanFromContext(ctx, "productUseCase.IndexAsync")
	defer span.Finish()
	span.LogFields(log.Object("product", product))

	dataBytes, err := serializer.Marshal(&product)
	if err != nil {
		return tracing.TraceWithErr(span, errors.Wrap(err, "serializer.Marshal"))
	}

	return p.amqpPublisher.Publish(
		ctx,
		p.cfg.ExchangeAndQueueBindings.IndexProductBinding.ExchangeName,
		p.cfg.ExchangeAndQueueBindings.IndexProductBinding.BindingKey,
		amqp.Publishing{
			Headers:   tracing.ExtractTextMapCarrierHeadersToAmqpTable(span.Context()),
			Timestamp: time.Now().UTC(),
			Body:      dataBytes,
		},
	)
}
```

<img src="https://i.postimg.cc/C1tsT4KB/Jaeger-UI-2022-08-15-13-45-30.png" alt="Jaeger"/>

The RabbitMQ consumer is listening queue and processing messages using **BulkIndexer** for [Bulk API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html)
which has better performance for indexing documents.

```go
func (c *consumer) ConsumeIndexDeliveries(ctx context.Context, deliveries <-chan amqp.Delivery, workerID int) func() error {
	return func() error {
		c.log.Infof("starting consumer workerID: %d, for queue deliveries: %s", workerID, c.cfg.ExchangeAndQueueBindings.IndexProductBinding.QueueName)

		for {
			select {
			case <-ctx.Done():
				c.log.Errorf("products consumer ctx done: %v", ctx.Err())
				return ctx.Err()

			case msg, ok := <-deliveries:
				if !ok {
					c.log.Errorf("deliveries channel closed for queue: %s", c.cfg.ExchangeAndQueueBindings.IndexProductBinding.QueueName)
					return errors.New("deliveries channel closed")
				}

				c.log.Infof("Consumer delivery: workerID: %d, msg data: %s, headers: %+v", workerID, string(msg.Body), msg.Headers)

				if err := c.bulkIndexProduct(ctx, msg); err != nil {
					c.log.Errorf("bulkIndexProduct err: %v", err)
					continue
				}

				c.log.Infof("Consumer <<<ACK>>> delivery: workerID: %d, msg data: %s, headers: %+v", workerID, string(msg.Body), msg.Headers)
				c.metrics.RabbitMQSuccessBatchInsertMessages.Inc()
			}
		}

	}
}

func (c *consumer) indexProduct(ctx context.Context, msg amqp.Delivery) error {
	span, ctx := opentracing.StartSpanFromContext(ctx, "consumer.indexProduct")
	defer span.Finish()

	var product domain.Product
	if err := serializer.Unmarshal(msg.Body, &product); err != nil {
		c.log.Errorf("indexProduct serializer.Unmarshal <<<Reject>>> err: %v", tracing.TraceWithErr(span, err))
		return msg.Reject(true)
	}
	if err := c.productUseCase.Index(ctx, product); err != nil {
		c.log.Errorf("indexProduct productUseCase.Index <<<Reject>>> err: %v", tracing.TraceWithErr(span, err))
		return msg.Reject(true)
	}
	return msg.Ack(true)
}

func (c *consumer) bulkIndexProduct(ctx context.Context, msg amqp.Delivery) error {
	ctx, span := tracing.StartRabbitConsumerTracerSpan(ctx, msg.Headers, "consumer.bulkIndexProduct")
	defer span.Finish()

	var product domain.Product
	if err := serializer.Unmarshal(msg.Body, &product); err != nil {
		c.log.Errorf("indexProduct serializer.Unmarshal <<<Reject>>> err: %v", tracing.TraceWithErr(span, err))
		return msg.Reject(true)
	}

	if err := c.bulkIndexer.Add(ctx, esutil.BulkIndexerItem{
		Index:      c.cfg.ElasticIndexes.ProductsIndex.Name,
		Action:     "index",
		DocumentID: product.ID,
		Body:       bytes.NewReader(msg.Body),
		OnSuccess: func(ctx context.Context, item esutil.BulkIndexerItem, item2 esutil.BulkIndexerResponseItem) {
			c.log.Debugf("bulk indexer onSuccess for index alias: %s", c.cfg.ElasticIndexes.ProductsIndex.Alias)
		},
		OnFailure: func(ctx context.Context, item esutil.BulkIndexerItem, item2 esutil.BulkIndexerResponseItem, err error) {
			if err != nil {
				c.log.Errorf("bulk indexer OnFailure err: %v", err)
			}
		},
	}); err != nil {
		c.log.Errorf("indexProduct bulkIndexer.Add <<<Reject>>> err: %v", tracing.TraceWithErr(span, err))
		return msg.Reject(true)
	}

	c.log.Infof("consumer <<<ACK>>> bulk indexer add product: %+v", product)
	return msg.Ack(true)
}
```

<img src="https://i.postimg.cc/5Nv9H5XT/Console-Dev-Tools-Elastic-2022-08-15-12-51-12.png" alt="Kibana"/>

The repository has the same methods for index and search.
For the search method, we use **should** **multi_match** query where we pass the original term and mapped to the opposite keyboard language layout search term.
Good practice for Elasticsearch is always use [alias](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html) for indexes.
The implementation for keyboard language layout converter is to load JSON file with mappings when the application starts, marshal it to the map and have one method for converting one language to another.

```go
func (k *keyboardLayoutsManager) GetOppositeLayoutWord(originalWord string) string {
	sb := k.sbPool.Get().(*strings.Builder)
	defer k.sbPool.Put(sb)
	sb.Reset()

	for _, c := range []rune(originalWord) {
		lowerCasedChar := strings.ToLower(string(c))
		if char, ok := k.keyMappings[lowerCasedChar]; ok {
			sb.WriteString(char)
		} else {
			sb.WriteString(lowerCasedChar)
		}
	}
	return sb.String()
}
```

<img src="https://i.postimg.cc/XJNz7x3s/Jaeger-UI-2022-08-14-13-48-26.png" alt="Jaeger"/>

```go
func (e *esRepository) Index(ctx context.Context, product domain.Product) error {
	span, ctx := opentracing.StartSpanFromContext(ctx, "esRepository.Index")
	defer span.Finish()
	span.LogFields(log.Object("product", product))

	dataBytes, err := serializer.Marshal(&product)
	if err != nil {
		return tracing.TraceWithErr(span, errors.Wrap(err, "serializer.Marshal"))
	}

	response, err := e.esClient.Index(
		e.cfg.ElasticIndexes.ProductsIndex.Alias,
		bytes.NewReader(dataBytes),
		e.esClient.Index.WithPretty(),
		e.esClient.Index.WithHuman(),
		e.esClient.Index.WithTimeout(indexTimeout),
		e.esClient.Index.WithContext(ctx),
		e.esClient.Index.WithDocumentID(product.ID),
	)
	if err != nil {
		return tracing.TraceWithErr(span, errors.Wrap(err, "esClient.Index"))
	}
	defer response.Body.Close()

	if response.IsError() {
		return tracing.TraceWithErr(span, errors.Wrap(errors.New(response.String()), "esClient.Index response error"))
	}

	e.log.Infof("document indexed: %s", response.String())
	return nil
}

func (e *esRepository) Search(ctx context.Context, term string, pagination *utils.Pagination) (*domain.ProductSearch, error) {
	span, ctx := opentracing.StartSpanFromContext(ctx, "esRepository.Search")
	defer span.Finish()
	span.LogFields(log.String("term", term))

	shouldQuery := map[string]any{
		"query": map[string]any{
			"bool": map[string]any{
				"should": []map[string]any{
					{
						"multi_match": map[string]any{
							"query":  term,
							"fields": searchFields,
						},
					},
					{
						"multi_match": map[string]any{
							"query":  e.keyboardLayoutManager.GetOppositeLayoutWord(term),
							"fields": searchFields,
						},
					},
					{
						"range": map[string]any{
							"count_in_stock": map[string]any{
								"gte": 0,
							},
						},
					},
				},
			},
		},
	}

	dataBytes, err := serializer.Marshal(&shouldQuery)
	if err != nil {
		return nil, tracing.TraceWithErr(span, errors.Wrap(err, "serializer.Marshal"))
	}
	
	response, err := e.esClient.Search(
		e.esClient.Search.WithContext(ctx),
		e.esClient.Search.WithIndex(e.cfg.ElasticIndexes.ProductsIndex.Alias),
		e.esClient.Search.WithBody(bufio.NewReader(bytes.NewReader(dataBytes))),
		e.esClient.Search.WithPretty(),
		e.esClient.Search.WithHuman(),
		e.esClient.Search.WithTimeout(searchTimeout),
		e.esClient.Search.WithSize(pagination.GetSize()),
		e.esClient.Search.WithFrom(pagination.GetOffset()),
	)
	if err != nil {
		return nil, tracing.TraceWithErr(span, errors.Wrap(err, "esClient.Search"))
	}
	defer response.Body.Close()

	if response.IsError() {
		return nil, tracing.TraceWithErr(span, errors.Wrap(errors.New(response.String()), "esClient.Search error"))
	}

	hits := esclient.EsHits[*domain.Product]{}
	if err := serializer.NewDecoder(response.Body).Decode(&hits); err != nil {
		return nil, tracing.TraceWithErr(span, errors.Wrap(err, "serializer.Decode"))
	}

	responseList := make([]*domain.Product, len(hits.Hits.Hits))
	for i, source := range hits.Hits.Hits {
		responseList[i] = source.Source
	}

	e.log.Infof("repository search result responseList: %+v", responseList)
	return &domain.ProductSearch{
		List:               responseList,
		PaginationResponse: utils.NewPaginationResponse(hits.Hits.Total.Value, pagination),
	}, nil
}
```

<img src="https://i.postimg.cc/LXM3p3ZS/Lens-2022-08-14-13-54-21.png" alt="Lens"/>

For Kubernetes prefer to use [minikube](https://minikube.sigs.k8s.io/docs/start/) with [helm](https://github.com/AleksK1NG/Go-elasticsearch-rabbitmq),
RabbitMQ, Elasticsearch, Jaeger, Kibana used same images as docker-compose file has, and [prometheus community chart](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack),
for correct working we have to add **ServiceMonitor** component for our microservice.
Working with k8s personally like to use [lens](https://k8slens.dev/) it has a friendly UI and many useful features.

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.searchMicroserviceName }}
  labels:
    app: {{ .Values.searchMicroserviceName }}
spec:
  replicas: {{ .Values.searchMicroserviceReplicas }}
  template:
    metadata:
      name: {{ .Values.searchMicroserviceName }}
      labels:
        app: {{ .Values.searchMicroserviceName }}
    spec:
      containers:
        - name: {{ .Values.searchMicroserviceName }}
          image: {{.Values.searchMicroserviceImage }}
          imagePullPolicy: Always
          resources:
            requests:
              memory: {{.Values.resources.requests.memory }}
              cpu: {{.Values.resources.requests.cpu }}
            limits:
              memory: {{.Values.resources.limits.memory }}
              cpu: {{.Values.resources.limits.cpu }}
          livenessProbe:
            httpGet:
              path: {{.Values.searchMicroserviceLivenessProbePath }}
              port: {{.Values.searchMicroserviceLivenessProbePort }}
            initialDelaySeconds: {{ .Values.searchMicroserviceInitialDelaySeconds }}
            periodSeconds: {{ .Values.searchMicroservicePeriodSeconds }}
          readinessProbe:
            httpGet:
              path: {{.Values.searchMicroserviceReadinessProbePath }}
              port: {{.Values.searchMicroserviceReadinessProbePort }}
            initialDelaySeconds: {{ .Values.searchMicroserviceInitialDelaySeconds }}
            periodSeconds: {{ .Values.searchMicroservicePeriodSeconds }}
          ports:
            - containerPort: {{.Values.searchMicroserviceHttpPort }}
              name: http
            - containerPort: {{.Values.searchMicroserviceMetricsPort }}
              name: metrics
            - containerPort: {{.Values.searchMicroserviceHealthcheckPort }}
              name: healthcheck
          env:
            - name: JAEGER_HOST_PORT
              value: {{ .Values.jaegerHotPost }}
            - name: ELASTIC_URL
              value: {{ .Values.elasticSearchURL }}
            - name: RABBITMQ_URI
              value: {{ .Values.rabbitMqURI }}
            - name: CONFIG_PATH
              value: "/search-config/search-config.yaml"
          volumeMounts:
            - name: config
              mountPath: "/search-config"
      restartPolicy: Always
      terminationGracePeriodSeconds: {{ .Values.searchMicroserviceTerminationGracePeriodSeconds }}
      volumes:
        - name: config
          configMap:
            name: {{ .Values.searchMicroserviceName }}-config-map
            items:
              - key: search-config.yaml
                path: search-config.yaml
  selector:
    matchLabels:
      app: {{ .Values.searchMicroserviceName }}

---

apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.searchMicroserviceName }}-service
  labels:
    app: {{ .Values.searchMicroserviceName }}
spec:
  type: ClusterIP
  selector:
    app: {{ .Values.searchMicroserviceName }}
  ports:
    - name: http
      port: {{.Values.searchMicroserviceHttpPort }}
      protocol: TCP
    - name: healthcheck
      port: {{.Values.searchMicroserviceHealthcheckPort }}
      protocol: TCP
      targetPort: metrics
    - name: metrics
      port: {{.Values.searchMicroserviceMetricsPort }}
      protocol: TCP
      targetPort: metrics

---

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    release: monitoring
  name: {{ .Values.searchMicroserviceName }}-service-monitor
  namespace: default
spec:
  selector:
    matchLabels:
      app: {{ .Values.searchMicroserviceName }}
  endpoints:
    - interval: 10s
      port: metrics
      path: {{.Values.prometheusPath }}
  namespaceSelector:
    matchNames:
      - default

---

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Values.searchMicroserviceName }}-config-map
data:
  search-config.yaml: |
    serviceName: search_microservice
    grpc:
      port: :5001
      development: true
    http:
      port: :{{ .Values.searchMicroserviceHttpPort }}
      development: {{ .Values.http.development }}
      basePath: {{ .Values.http.basePath }}
      productsPath: {{ .Values.http.productsPath }}
      debugErrorsResponse: {{ .Values.http.debugErrorsResponse }}
      ignoreLogUrls: {{ .Values.http.ignoreLogUrls }}
    probes:
      readinessPath: {{ .Values.searchMicroserviceReadinessProbePath }}
      livenessPath: {{ .Values.searchMicroserviceLivenessProbePath }}
      port: :{{ .Values.searchMicroserviceHealthcheckPort }}
      pprof: :6001
      prometheusPath: {{ .Values.prometheusPath }}
      prometheusPort: :{{.Values.searchMicroserviceMetricsPort }}
      checkIntervalSeconds: 10
    logger:
      level: {{ .Values.searchMicroserviceLogging.level }}
      devMode: {{ .Values.searchMicroserviceLogging.devMode }}
      encoder: {{ .Values.searchMicroserviceLogging.encoder }}
    jaeger:
      enable: true
      serviceName: {{ .Values.searchMicroserviceName }}
      hostPort: {{ .Values.jaegerHotPost }}
      logSpans: false
    timeouts:
      postgresInitMilliseconds: 1500
      postgresInitRetryCount: 3
    elasticSearch:
      addresses: [ {{ .Values.elasticSearchURL }} ]
      username: ""
      password: ""
      apiKey: ""
      enableLogging: false
    elasticIndexes:
      products:
        path: {{ .Values.elasticIndexes.products.path }}
        name: {{ .Values.elasticIndexes.products.name }}
        alias: {{ .Values.elasticIndexes.products.alias }}
    rabbitmq:
      uri: {{ .Values.rabbitMqURI }}
    exchangeAndQueueBindings:
      indexProductBinding:
        exchangeName: {{.Values.exchangeAndQueueBindings.indexProductBinding.exchangeName }}
        exchangeKind: {{.Values.exchangeAndQueueBindings.indexProductBinding.exchangeKind }}
        queueName: {{.Values.exchangeAndQueueBindings.indexProductBinding.queueName }}
        bindingKey: {{.Values.exchangeAndQueueBindings.indexProductBinding.bindingKey }}
        concurrency: {{.Values.exchangeAndQueueBindings.indexProductBinding.concurrency }}
        consumer: {{.Values.exchangeAndQueueBindings.indexProductBinding.consumer }}
    bulkIndexer:
      numWorkers: {{ .Values.bulkIndexer.numWorkers }}
      flushBytes: {{ .Values.bulkIndexer.flushBytes }}
      flushIntervalSeconds: {{ .Values.bulkIndexer.flushIntervalSeconds }}
      timeoutMilliseconds: {{ .Values.bulkIndexer.timeoutMilliseconds }}
```

More details and source code of the full project you can find [here](https://github.com/AleksK1NG/Go-elasticsearch-rabbitmq),
of course, in real-world applications, full-text search and business requirements can be much more complicated and for example includes machine learning, etc.
I hope this article is usefully and helpfully, and be happy to receive any feedback or questions, feel free to [contact](https://www.linkedin.com/in/alexander-bryksin/) [me](https://github.com/AleksK1NG) by [email](alexander.bryksin@yandex.ru) or any [messengers](https://t.me/AlexanderBryksin) :)