ElasticSearch search in attachments

Here is simplest possible demo of what elasticsearch can do:

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
    <title>Attachments Demo</title>
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="bower_components/fontawesome/css/font-awesome.css" />
    <style>
      html,
      body {
        height: 100vh;
      }

      mark {
        background: yellow;
      }

      .drop:after {
        content: 'Drop file here';
        pointer-events: none;
        position: fixed;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        background-color: #999;
        background-image: radial-gradient(rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 1) 100%);
        opacity: 0.5;
        color: white;
        line-height: 100vh;
        text-align: center;
        font-size: 10vh;
        text-transform: uppercase;
        text-shadow: 1px 1px 5px #000;
      }
    </style>
  </head>
  <body>
    <div class="container" style="padding: 15px">
      <form class="row" data-bind="submit: reload">
        <p class="col-md-10">
          <input type="search" class="form-control" data-bind="value: q" />
        </p>
        <p class="col-md-2">
          <button class="btn btn-default btn-block" data-bind="click: reload">Filter</button>
        </p>
      </form>

      <ul class="list-group" data-bind="foreach: hits">
        <li class="list-group-item">
          <div class="media">
            <div class="media-body">
              <i data-bind="css: icon"></i>
              <strong data-bind="text: _source.name"></strong>
            </div>
            <div class="media-right">
              <a class="text-danger" href="#" data-bind="click: $root.delete">delete</a>
            </div>
          </div>
          <div data-bind="if: hasHighlights">
            <ol data-bind="foreach: highlight.content">
              <li data-bind="html: $data"></li>
            </ol>
          </div>
        </li>
      </ul>
    </div>
    <script src="bower_components/jquery/dist/jquery.js"></script>
    <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
    <script src="bower_components/knockout/dist/knockout.debug.js"></script>
    <script src="bower_components/elasticsearch/elasticsearch.js"></script>
    <script>
      function Hit(data) {
        var self = this

        ko.utils.extend(self, data)

        self.hasHighlights = self.highlight && self.highlight.content && self.highlight.content.length > 0

        if (
          self._source.type === 'application/msword' ||
          self._source.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        ) {
          self.icon = 'fa fa-file-word-o'
        } else {
          self.icon = 'fa fa-file-text-o'
        }
      }

      function Model() {
        var self = this

        self.index = 'attachments'
        self.type = 'document' // need to be replaced in mapping also

        self.client = new elasticsearch.Client({
          host: 'localhost:9200',
          log: 'trace'
        })

        self.q = ko.observable('')
        self.hits = ko.observableArray()

        self.reload = function () {
          var body = {
            index: self.index,
            type: self.type,
            body: {
              _source: {
                exclude: ['content']
              }
            }
          }

          if (self.q() && self.q().trim().length > 0) {
            body.body.query = {
              query_string: {
                query: self.q()
              }
            }

            body.body.highlight = {
              pre_tags: ['<mark>'],
              post_tags: ['</mark>'],
              fields: {
                content: {}
              }
            }
          }

          self.client.search(body).then(function (body) {
            self.hits(
              body.hits.hits.map(function (hit) {
                return new Hit(hit)
              })
            )
          }, alert)
        }

        self.delete = function (data) {
          self.client.delete(
            {
              index: self.index,
              type: self.type,
              id: data._id,
              ignore: [404]
            },
            function (err, body) {
              if (body.found) {
                self.hits.remove(data)
              }
            }
          )
        }

        function nop(event) {
          event.stopPropagation()
          event.preventDefault()
          event.dataTransfer.dropEffect = 'copy'
          document.body.className = event.type === 'dragleave' ? '' : 'drop'
        }
        document.body.addEventListener('dragenter', nop)
        document.body.addEventListener('dragover', nop, false)
        document.body.addEventListener('dragleave', nop)
        document.body.addEventListener(
          'drop',
          function (event) {
            event.stopPropagation()
            event.preventDefault()
            ;[].forEach.call(event.dataTransfer.files, function (file) {
              var reader = new FileReader()
              reader.addEventListener('load', function (event) {
                self.client
                  .create({
                    index: self.index,
                    type: self.type,
                    body: {
                      name: file.name,
                      date: new Date(file.lastModified).toISOString(),
                      type: file.type,
                      content: event.target.result.substr(event.target.result.indexOf(',') + 1)
                    }
                  })
                  .then(function (body) {
                    body._source = {
                      name: file.name,
                      date: new Date(file.lastModified).toISOString(),
                      type: file.type
                    }
                    self.hits.push(new Hit(body))
                  })
              })
              reader.readAsDataURL(file)
              document.body.className = ''
            })
          },
          false
        )

        self.mappings = {
          index: self.index,
          body: {
            mappings: {}
          }
        }

        self.mappings.body.mappings[self.type] = {
          properties: {
            name: { type: 'string' },
            date: { type: 'date' },
            type: { type: 'string' },
            content: {
              type: 'attachment',
              fields: {
                content: { store: 'yes' },
                author: { store: 'yes' },
                title: { store: 'yes' },
                date: { store: 'yes' },
                keywords: { store: 'yes' },
                _name: { store: 'yes' },
                _content_type: { store: 'yes' }
              }
            }
          }
        }

        self.client.indices.exists({ index: self.index }, function (body, exists) {
          if (!exists) {
            self.client.indices.create(self.mappings)
          } else {
            self.reload()
          }
        })
      }

      var model = new Model()
      ko.applyBindings(model)
    </script>
  </body>
</html>

bower.json

{
  "name": "attachments_demo",
  "version": "0.0.0",
  "dependencies": {
    "jquery": "~2.1.4",
    "bootstrap": "~3.3.5",
    "knockout": "~3.3.0",
    "fontawesome": "~4.3.0",
    "elasticsearch": "~5.0.0"
  }
}

elasticsearch.yml

cluster.name: attachments
node.name: ${COMPUTERNAME}
plugin.mandatory: mapper-attachments
http.cors.enabled: true
index.number_of_shards: 1
index.number_of_replicas: 0

For this demo to work mapper-attachments plugin should be installed.