UNPKG

@sugarcube/plugin-elasticsearch

Version:

Use [Elasticsearch](https://www.elastic.co/products/elasticsearch) for SugarCube data.

419 lines (365 loc) 10.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Elastic = exports.queryExisting = exports.queryOne = exports.queryByIds = exports.bulk = exports.query = exports.reindex = exports.toMsg = exports.toHeader = exports.connect = void 0; var _fp = require("lodash/fp"); var _dashp = require("dashp"); var _es = require("es6"); var _es2 = require("es7"); var _nodeFetch = _interopRequireDefault(require("node-fetch")); var _core = require("@sugarcube/core"); var _utils = require("./utils"); var _mappings = _interopRequireDefault(require("./mappings")); var _queries = _interopRequireDefault(require("./queries")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /* eslint-disable no-plusplus */ const { curry2, curry3, curry4, curry6 } = _core.utils; const esVersion = async node => { const { version } = await (0, _nodeFetch.default)(node).then(resp => resp.json()); return parseInt(version.number[0], 10); }; const mergeMappings = async (client, customMappings = {}) => { const { body } = await client.info(); const version = parseInt(body.version.number[0], 10); switch (version) { case 6: { return Object.assign(_mappings.default, customMappings); } case 7: { const { mappings, ...rest } = _mappings.default; return Object.assign(rest, { mappings: mappings._doc }, customMappings); } default: { throw new Error(`Elasticsearch version ${version} not supported.`); } } }; // ES6 returns total as number, ES7 as an object with a value sttribute. const normalizeTotal = body => { return (0, _fp.isPlainObject)(body.hits.total) ? body.hits.total.value : body.hits.total; }; const connect = async node => { let Client; const version = await esVersion(node); switch (version) { case 6: { Client = _es.Client; break; } case 7: { Client = _es2.Client; break; } default: { throw new Error(`Elasticsearch version ${version} not supported.`); } } return new Client({ node, requestTimeout: 60 * 1000 }); }; exports.connect = connect; const toHeader = (index, unit) => ({ _index: index, _id: unit._sc_id_hash }); exports.toHeader = toHeader; const toMsg = (index, unit) => (0, _utils.stripUnderscores)((toHeader(index, unit), { body: unit })); exports.toMsg = toMsg; const createIndex = curry3("createIndex", async (index, mapping, client) => { const body = await mergeMappings(client, mapping); const { body: aliasExists } = await client.indices.existsAlias({ name: index }); const { body: indexExists } = await client.indices.exists({ index }); if (aliasExists || indexExists) return (0, _dashp.ofP)(null); await client.indices.create({ index: `${index}-1`, body }); return client.indices.putAlias({ index: `${index}-1`, name: index }); }); /* * Public API */ const reindex = curry6("reindex", async (index, host, port, toIndex, client, customMappings) => { await createIndex(toIndex, customMappings, client); await client.reindex({ body: _queries.default.reindex(index, host, port, toIndex), refresh: true, waitForCompletion: false }); return [null, {}]; }); exports.reindex = reindex; const query = curry4("query", async (index, reqBody, amount, client, customMappings) => { await createIndex(index, customMappings, client); const allData = []; let meta = { took: 0, total: 0 }; const responseQueue = []; responseQueue.push(await client.search({ index, body: reqBody, size: 2000, scroll: "60s" })); while (responseQueue.length) { const { body } = responseQueue.shift(); // ES7 changed the return value of body.hits.total const total = normalizeTotal(body); while (body.hits.hits.length > 0) { const unit = body.hits.hits.shift(); allData.push(Object.assign((0, _utils.unstripify)(unit._source), { _sc_elastic_score: unit.score }, unit.highlight != null ? { _sc_elastic_highlights: (0, _utils.unstripify)(unit.highlight) } : {})); } meta = Object.assign({}, meta, body.timed_out ? { timedOut: true } : {}, { took: meta.took + body.took, total }); if (amount != null && allData.length >= amount || total === allData.length) { break; } responseQueue.push( // eslint-disable-next-line no-await-in-loop await client.scroll({ scrollId: body._scroll_id, scroll: "30s" })); } return [allData, meta]; }); exports.query = query; const bulk = curry4("bulk", async (index, ops, client, customMappings) => { const batchSize = 500; const toIndex = []; const toUpdate = []; for (let i = 0; i < (ops.index || []).length; i++) { const unit = ops.index[i]; toIndex.push({ index: toHeader(index, unit) }); toIndex.push((0, _utils.stripUnderscores)(unit)); } for (let i = 0; i < (ops.update || []).length; i++) { const unit = ops.update[i]; toUpdate.push({ update: toHeader(index, unit) }); toUpdate.push({ doc: (0, _utils.stripUnderscores)(unit) }); } // Ensure the index is created. await createIndex(index, customMappings, client); // Run the bulk requests const responseQueue = []; const errors = []; let meta = { took: 0, batches: 0, batchSize }; const [firstChunk, ...chunks] = (0, _fp.chunk)(batchSize, toIndex.concat(toUpdate)); responseQueue.push(await client.bulk({ index, body: firstChunk, type: "_doc", refresh: true })); while (responseQueue.length) { const { body } = responseQueue.shift(); const { took, items } = body; for (let i = 0; i < (items || []).length; i++) { const item = items[i]; const op = item.index || item.update || item.create || item.delete; const result = op.result || "error"; const count = meta[result] || 0; if (op.error != null) errors.push({ id: op._id, error: `${op.error.type}: ${op.error.reason} (${op.error.caused_by != null ? op.error.caused_by.reason : ""})` }); meta = Object.assign({}, meta, { took: meta.took + took, [result]: count + 1 }); } if (chunks.length === 0) { break; } const nextChunk = chunks.shift(); responseQueue.push( // eslint-disable-next-line no-await-in-loop await client.bulk({ index, body: nextChunk, type: "_doc", refresh: true })); } return [errors, meta]; }); exports.bulk = bulk; const queryByIds = curry3("queryByIds", async (index, ids, client, customMappings) => { const batchSize = 2500; const responses = await (0, _dashp.collectP)(idsChunk => { const reqBody = _queries.default.byIds(idsChunk); return query(index, reqBody, null, client, customMappings); }, (0, _fp.chunk)(batchSize, ids)); return responses.reduce(([data, meta], response) => { const { took, total } = response[1]; return [data.concat(response[0]), Object.assign({}, meta, { took: took + meta.took, total: total + meta.total })]; }, [[], { took: 0, total: 0, batches: responses.length, batchSize }]); }); exports.queryByIds = queryByIds; const queryOne = curry3("queryOne", async (index, id, client, customMappings) => { // Ensure the index is created. await createIndex(index, customMappings, client); const { _version: version, _type: type, _source: data } = await client.get({ index, id, type: "_all" }).body; return [data, { version, type }]; }); exports.queryOne = queryOne; const queryExisting = curry3("queryExisting", async (index, ids, client, customMappings) => { const batchSize = 5000; await createIndex(index, customMappings, client); const responses = await (0, _dashp.collectP)(idsChunk => { const reqBody = _queries.default.existing(idsChunk); return query(index, reqBody, null, client, customMappings); }, (0, _fp.chunk)(batchSize, ids)); return responses.reduce(([data, meta], response) => { const { took, total } = response[1]; return [data.concat(response[0].map(unit => unit._sc_id_hash)), Object.assign({}, meta, { took: took + meta.took, total: total + meta.total })]; }, [[], { took: 0, total: 0, batches: responses.length, batchSize }]); }); exports.queryExisting = queryExisting; const Elastic = { Do: curry2("ElasticDo", async (G, { host, port, mappings }) => { const node = `http://${host}:${port}`; const client = await connect(node); const api = { bulk, query, queryByIds, queryOne, queryExisting, reindex }; const customMappings = (0, _utils.stripUnderscores)(mappings || {}); const generator = G(api); let data; let history = []; const chain = async nextG => { const { done, value } = await nextG.next(data); if (done) return (0, _dashp.ofP)([value || data, history]); // All curried function names have the format of <name>-<int> where // <int> is the number of missing arguments. For a prettier output in // the history strip -<int> from the name. const prettyName = value.name.replace(/-.*$/, ""); let result; let meta; try { [result, meta] = await value(client, customMappings); } catch (e) { const error = JSON.stringify({ status: (0, _fp.get)("meta.statusCode", e), body: (0, _fp.get)("meta.meta.request.params.body", e), href: (0, _fp.get)("meta.meta.request.params.url.href", e), type: (0, _fp.get)("meta.body.error.type", e), reason: (0, _fp.get)("meta.body.error.reason", e), line: (0, _fp.get)("meta.body.error.line", e), col: (0, _fp.get)("meta.body.error.col", e), method: (0, _fp.get)("meta.meta.request.params.method", e), path: (0, _fp.get)("meta.meta.request.params.path", e), qs: (0, _fp.get)("meta.meta.request.params.querystring", e) }); e.message = `\`${prettyName}\` in \`${G.name}\`: ${error}`; throw e; } history = history.concat([[prettyName, meta]]); data = result; return chain(nextG); }; return (0, _dashp.ofP)(chain(generator)); }) }; exports.Elastic = Elastic;