I recently wrote a post about implement Elasticsearch to allow your readers to search in the contents of your self-hosted Ghost blog "Elasticsearch as search engine for your Ghost blog". In this post, I will explain to you how to expose and use your search engine.

Expose your search endpoint with a reverse proxy

Based on my previous post, the Elasticsearch endpoint has its own URL. That means, if you want to use it in a production environment, all requests must be against another public URL. Example:

  • The URL of your Ghost blog is: https://blog.mydomain.com/
  • The URL of your Elasticsearch server is: https://elasticsearch.mydomain.com:9200/blog_search/post/_search

Some CORS security conflicts with the browsers may occur if you have another subdomain. Moreover, with a public Elasticsearch endpoint, it is possible to request all indexes of your search service. In this case, we only have /blog_search/ and if you want to use your engine with other services, that can be considered as a security leak. Example:

  • If you use the URL https://elasticsearch.mydomain.com:9200/_search: you can access any index of your search
  • If you use the URL https://elasticsearch.mydomain.com:9200/blog_search/_search: you can only access the index blog_search

Expose your Elasticsearch through a reverse proxy can help you to:

  • Force the access to a specific index
  • Redirect search queries from your blog URL to your Elasticsearch server
  • Not directly expose your search endpoint on the internet

Docker Nginx Proxy

Thanks to the jwilder/nginx-proxy docker image, it is possible to host several docker containers on one physical server and to customize the configuration of the external access for all or per container.


  • A Docker internal network that allows connecting all containers with no external access (recommended). Example:
    docker network create --driver=bridge --attachable --scope=local --internal --subnet= no-internet
  • A Docker network that allows access from the Internet (that can be the default one)
  • A folder named vhost that will be mapped with the nginx-proxy container and that will contain custom location configurations (use a folder to store all configuration files is more flexible and maintainable than binding each configuration file)

Docker file

With these prerequisites in mind, here is the nginx-proxy compose file:

version: '3.7'

    image: jwilder/nginx-proxy
      - "80:80"
      - "443:443"
      - "/var/run/docker.sock:/tmp/docker.sock:ro"
      - type: bind
        source: ./vhost
        target: /etc/nginx/vhost.d
      - no-internet
      - default
    restart: always

      name: no-internet
Docker compose file for Nginx

It is important to note that the nginx container must have access to the default (internet) network and the local one (no-internet) too.

You can start the container:

docker-compose -f nginx-proxy-compose.yml up -d

Virtual Host Location

It is time to create your location file myblog.mydomain.com_location that allows to redirect all requests to your Elasticsearch server:

# Search API delivered by ElasticSearch
location ^~ /api/search {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    proxy_set_header Connection "Keep-Alive";
    proxy_set_header Proxy-Connection "Keep-Alive";
    proxy_pass http://ghost-elasticsearch:9200/blog_search/post/_search;
    proxy_read_timeout 600;
Nginx custom location file
  • The file name must be named like your Ghost blog URL (same as the value of your VIRTUAL_HOST docker environment variable used by your Ghost blog container) and saved into your vhost folder
  • In this case, the endpoint will be /api/search.
    That means if your blog URL is https://myblog.mydomain.com, your Elasticsearch endpoint will be https://myblog.mydomain.com/api/search
  • The last important thing is the proxy_pass that redirects all requests to your Elasticsearch and more particularly to the index of your blog search 😎

Update your Elastic stack file

Based on my post Elasticsearch as search engine for your Ghost blog, I shared the docker-compose file for Elasticsearch with you. You must change the file to restrict the external network and specify the local network only:

version: '3.7'

      - no-internet
      - no-internet

      name: no-internet
Limit access to the local network only for Elasticsearch

Once the changes made, down and up your Elasticsearch container.

Ensure the following connexions between the servers:

  • Elasticsearch <-> Logstash
  • Logstash <-> Ghost blog database
  • Ghost blog <-> Ghost blog database
  • Nginx <-> Ghost blog
  • Nginx <-> Elastisearch

Maybe a little schema to be clearer? Ok, here the new HTTP feed redirection:

You are now ready to request your Elasticsearch on safe use πŸ˜‰

JavaScript code implementation

At this point, I am pretty sure you can not wait anymore to test your configuration and know how you can launch requests via JavaScript on your new search engine πŸ™‚

Without further ado, the part of JavaScript to launch the requests against your search is:

function sendQuerySearch(keyword) {
  /* Because we are using the same hostname, we can use relative URL */
  var t = '/api/search';
  var s = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
  /* Open an async POST request */
  s.open('POST', t, true);
  s.setRequestHeader("Accept", "application/json;odata=verbose");
  s.setRequestHeader("Content-Type", "application/json");
  /* If you enabled the authentication on your Elastic search enable credentials */
  s.withCredentials = true;
  /* Use basic authentication
   * elasticsearch_auth  match with btoa("elastic:yourpassword");
  s.setRequestHeader("Authorization", "Basic " + elasticsearch_auth);
  s.onreadystatechange = function () {
    var status;
    if (s.readyState == 4) { // `DONE`
      status = s.status;
      if (status == 200) {
        /* retrieve the request answer */
        var data = JSON.parse(s.responseText);
      } else {
        /* Something is wrong. Add console to see what is happening */
  /* Body request */
  var body = {
      "size": 500,
      "from": 0,
      "query": {
          "query_string": {
              "query": keyword,
              "default_operator": "AND"
      "post_filter": {
          "term": {
              "status": "published"
          "fields": {
              "title": { },
              "custom_excerpt": { }
  /* Send the request against Elasticsearch api */
JavaScript function to request Elasticsearch
  • size: number of results returned by the search engine. It can be increased or you can manage the pagination with the from property
  • from: return results of the search from a specific index
  • query:
    • query: keyword that corresponds to the search performed by the user (the value from the HTML input)
    • default_operator: the operator between each word of the search
  • post_filter: filter the posts/pages by their status to return only published resources
  • highlight: one of the powerful features of a real search engine is to return the fields with the highlighted words from the content in accordance with the search keywords

For more information about the Elasticsearch body request, see the documentation at Β https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html

You are now ready to perform search requests and get the results from JavaScript.

What's next

In the next part, I will share with you how to implement a search interface that looks almost like the Ghost Admin one.

Hoping this post will help you πŸ˜‰

You may also be interested in