├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── bin └── www ├── elasticsearch.js ├── jsconfig.json ├── package.json ├── public └── stylesheets │ └── style.css ├── routes ├── documents.js ├── index.js └── users.js └── views ├── error.jade ├── index.jade └── layout.jade /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Raanan Weber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting started with elasticsearch 2.x and Express.js 2 | 3 | A simple demo showing how simple it is to use elasticsearch 2.x with express.js. 4 | 5 | You can read the blog post at https://blog.raananweber.com/2015/11/24/simple-autocomplete-with-elasticsearch-and-node-js/ 6 | 7 | start the app with: 8 | 9 | ``` 10 | npm start 11 | ``` 12 | 13 | # License 14 | 15 | http://raananw.mit-license.org/ 16 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | 8 | var routes = require('./routes/index'); 9 | var users = require('./routes/users'); 10 | var documents = require('./routes/documents'); 11 | 12 | var app = express(); 13 | 14 | // view engine setup 15 | app.set('views', path.join(__dirname, 'views')); 16 | app.set('view engine', 'jade'); 17 | 18 | // uncomment after placing your favicon in /public 19 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 20 | app.use(logger('dev')); 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | app.use(cookieParser()); 24 | app.use(express.static(path.join(__dirname, 'public'))); 25 | 26 | app.use('/', routes); 27 | app.use('/users', users); 28 | app.use('/documents', documents); 29 | 30 | // catch 404 and forward to error handler 31 | app.use(function (req, res, next) { 32 | var err = new Error('Not Found'); 33 | err.status = 404; 34 | next(err); 35 | }); 36 | 37 | // error handlers 38 | 39 | // development error handler 40 | // will print stacktrace 41 | if (app.get('env') === 'development') { 42 | app.use(function (err, req, res, next) { 43 | res.status(err.status || 500); 44 | res.render('error', { 45 | message: err.message, 46 | error: err 47 | }); 48 | }); 49 | } 50 | 51 | // production error handler 52 | // no stacktraces leaked to user 53 | app.use(function (err, req, res, next) { 54 | res.status(err.status || 500); 55 | res.render('error', { 56 | message: err.message, 57 | error: {} 58 | }); 59 | }); 60 | 61 | var elastic = require('./elasticsearch'); 62 | elastic.indexExists().then(function (exists) { 63 | if (exists) { 64 | return elastic.deleteIndex(); 65 | } 66 | }).then(function () { 67 | return elastic.initIndex().then(elastic.initMapping).then(function () { 68 | //Add a few book titles for the autocomplete 69 | //elasticsearch offers a bulk functionality as well, but this is for a different time 70 | var promises = [ 71 | 'Thing Explainer', 72 | 'The Internet Is a Playground', 73 | 'The Pragmatic Programmer', 74 | 'The Hitchhikers Guide to the Galaxy', 75 | 'Trial of the Clone', 76 | 'All Quiet on the Western Front', 77 | 'The Animal Farm', 78 | 'The Circle' 79 | ].map(function (bookTitle) { 80 | return elastic.addDocument({ 81 | title: bookTitle, 82 | content: bookTitle + " content!", 83 | metadata: { 84 | titleLength: bookTitle.length 85 | } 86 | }); 87 | }); 88 | return Promise.all(promises); 89 | }); 90 | }); 91 | 92 | 93 | module.exports = app; 94 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('autocompleter:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /elasticsearch.js: -------------------------------------------------------------------------------- 1 | var elasticsearch = require('elasticsearch'); 2 | 3 | var elasticClient = new elasticsearch.Client({ 4 | host: 'localhost:9200', 5 | log: 'info' 6 | }); 7 | 8 | var indexName = "randomindex"; 9 | 10 | /** 11 | * Delete an existing index 12 | */ 13 | function deleteIndex() { 14 | return elasticClient.indices.delete({ 15 | index: indexName 16 | }); 17 | } 18 | exports.deleteIndex = deleteIndex; 19 | 20 | /** 21 | * create the index 22 | */ 23 | function initIndex() { 24 | return elasticClient.indices.create({ 25 | index: indexName 26 | }); 27 | } 28 | exports.initIndex = initIndex; 29 | 30 | /** 31 | * check if the index exists 32 | */ 33 | function indexExists() { 34 | return elasticClient.indices.exists({ 35 | index: indexName 36 | }); 37 | } 38 | exports.indexExists = indexExists; 39 | 40 | function initMapping() { 41 | return elasticClient.indices.putMapping({ 42 | index: indexName, 43 | type: "document", 44 | body: { 45 | properties: { 46 | title: { type: "string" }, 47 | content: { type: "string" }, 48 | suggest: { 49 | type: "completion", 50 | analyzer: "simple", 51 | search_analyzer: "simple", 52 | payloads: true 53 | } 54 | } 55 | } 56 | }); 57 | } 58 | exports.initMapping = initMapping; 59 | 60 | function addDocument(document) { 61 | return elasticClient.index({ 62 | index: indexName, 63 | type: "document", 64 | body: { 65 | title: document.title, 66 | content: document.content, 67 | suggest: { 68 | input: document.title.split(" "), 69 | output: document.title, 70 | payload: document.metadata || {} 71 | } 72 | } 73 | }); 74 | } 75 | exports.addDocument = addDocument; 76 | 77 | function getSuggestions(input) { 78 | return elasticClient.suggest({ 79 | index: indexName, 80 | type: "document", 81 | body: { 82 | docsuggest: { 83 | text: input, 84 | completion: { 85 | field: "suggest", 86 | fuzzy: true 87 | } 88 | } 89 | } 90 | }) 91 | } 92 | exports.getSuggestions = getSuggestions; -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6" 4 | } 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocompleter", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "cookie-parser": "~1.3.5", 11 | "debug": "~2.2.0", 12 | "elasticsearch": "^9.0.2", 13 | "express": "~4.13.1", 14 | "jade": "~1.11.0", 15 | "morgan": "~1.6.1", 16 | "serve-favicon": "~2.3.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /routes/documents.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var elastic = require('../elasticsearch'); 5 | 6 | /* GET suggestions */ 7 | router.get('/suggest/:input', function (req, res, next) { 8 | elastic.getSuggestions(req.params.input).then(function (result) { res.json(result) }); 9 | }); 10 | 11 | /* POST document to be indexed */ 12 | router.post('/', function (req, res, next) { 13 | elastic.addDocument(req.body).then(function (result) { res.json(result) }); 14 | }); 15 | 16 | module.exports = router; -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET users listing. */ 5 | router.get('/', function(req, res, next) { 6 | res.send('respond with a resource'); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | --------------------------------------------------------------------------------