UNPKG

@jmaitrehenry/elastic-builder

Version:

A JavaScript implementation of the elasticsearch Query DSL

601 lines (530 loc) 21.6 kB
'use strict'; var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); var _createClass2 = require('babel-runtime/helpers/createClass'); var _createClass3 = _interopRequireDefault(_createClass2); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var has = require('lodash.has'), isEmpty = require('lodash.isempty'), isNil = require('lodash.isnil'), isString = require('lodash.isstring'); var Query = require('./query'); var _require = require('./util'), checkType = _require.checkType, invalidParam = _require.invalidParam, recursiveToJSON = _require.recursiveToJSON; var ES_REF_URL = 'https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html'; var invalidEncoderParam = invalidParam(ES_REF_URL, 'encoder', "'default' or 'html'"); var invalidTypeParam = invalidParam(ES_REF_URL, 'type', "'plain', 'postings' or 'fvh'"); var invalidFragmenterParam = invalidParam(ES_REF_URL, 'fragmenter', "'simple' or 'span'"); /** * Allows to highlight search results on one or more fields. In order to * perform highlighting, the actual content of the field is required. If the * field in question is stored (has store set to yes in the mapping), it will * be used, otherwise, the actual _source will be loaded and the relevant * field will be extracted from it. * * If no term_vector information is provided (by setting it to * `with_positions_offsets` in the mapping), then the plain highlighter will be * used. If it is provided, then the fast vector highlighter will be used. * When term vectors are available, highlighting will be performed faster at * the cost of bigger index size. * * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html) * * @example * const reqBody = esb.requestBodySearch() * .query(esb.matchAllQuery()) * .highlight(esb.highlight('content')); * * @example * const highlight = esb.highlight() * .numberOfFragments(3) * .fragmentSize(150) * .fields(['_all', 'bio.title', 'bio.author', 'bio.content']) * .preTags('<em>', '_all') * .postTags('</em>', '_all') * .numberOfFragments(0, 'bio.title') * .numberOfFragments(0, 'bio.author') * .numberOfFragments(5, 'bio.content') * .scoreOrder('bio.content'); * * highlight.toJSON() * { * "number_of_fragments" : 3, * "fragment_size" : 150, * "fields" : { * "_all" : { "pre_tags" : ["<em>"], "post_tags" : ["</em>"] }, * "bio.title" : { "number_of_fragments" : 0 }, * "bio.author" : { "number_of_fragments" : 0 }, * "bio.content" : { "number_of_fragments" : 5, "order" : "score" } * } * } * * @param {string|Array=} fields An optional field or array of fields to highlight. */ var Highlight = function () { // eslint-disable-next-line require-jsdoc function Highlight(fields) { (0, _classCallCheck3.default)(this, Highlight); this._fields = {}; this._highlight = { fields: this._fields }; // Does this smell? if (isNil(fields)) return; if (isString(fields)) this.field(fields);else this.fields(fields); } /** * Private function to set field option * * @param {string|null} field * @param {string} option * @param {string} val * @private */ (0, _createClass3.default)(Highlight, [{ key: '_setFieldOption', value: function _setFieldOption(field, option, val) { if (isNil(field)) { this._highlight[option] = val; return; } this.field(field); this._fields[field][option] = val; } /** * Allows you to set a field that will be highlighted. The field is * added to the current list of fields. * * @param {string} field A field name. * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'field', value: function field(_field) { if (!isNil(_field) && !has(this._fields, _field)) { this._fields[_field] = {}; } return this; } /** * Allows you to set the fields that will be highlighted. All fields are * added to the current list of fields. * * @param {Array<string>} fields Array of field names. * @returns {Highlight} returns `this` so that calls can be chained * @throws {TypeError} If `fields` is not an instance of Array */ }, { key: 'fields', value: function fields(_fields) { var _this = this; checkType(_fields, Array); _fields.forEach(function (field) { return _this.field(field); }); return this; } /** * Sets the pre tags for highlighted fragments. You can apply the * tags to a specific field by passing the optional field name parameter. * * @example * const highlight = esb.highlight('_all') * .preTags('<tag1>') * .postTags('</tag1>'); * * @example * const highlight = esb.highlight('_all') * .preTags(['<tag1>', '<tag2>']) * .postTags(['</tag1>', '</tag2>']); * * @param {string|Array} tags * @param {string=} field * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'preTags', value: function preTags(tags, field) { this._setFieldOption(field, 'pre_tags', isString(tags) ? [tags] : tags); return this; } /** * Sets the post tags for highlighted fragments. You can apply the * tags to a specific field by passing the optional field name parameter. * * @example * const highlight = esb.highlight('_all') * .preTags('<tag1>') * .postTags('</tag1>'); * * @example * const highlight = esb.highlight('_all') * .preTags(['<tag1>', '<tag2>']) * .postTags(['</tag1>', '</tag2>']); * * @param {string|Array} tags * @param {string=} field * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'postTags', value: function postTags(tags, field) { this._setFieldOption(field, 'post_tags', isString(tags) ? [tags] : tags); return this; } /** * Sets the styled schema to be used for the tags. * * styled - 10 `<em>` pre tags with css class of hltN, where N is 1-10 * * @example * const highlight = esb.highlight('content').styledTagsSchema(); * * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'styledTagsSchema', value: function styledTagsSchema() { // This is a special case as it does not map directly to elasticsearch DSL // This is written this way for ease of use this._highlight.tags_schema = 'styled'; return this; } /** * Sets the order of highlight fragments to be sorted by score. You can apply the * score order to a specific field by passing the optional field name parameter. * * @example * const highlight = esb.highlight('content').scoreOrder() * * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'scoreOrder', value: function scoreOrder(field) { // This is a special case as it does not map directly to elasticsearch DSL // It is written this way for ease of use this._setFieldOption(field, 'order', 'score'); return this; } /** * Sets the size of each highlight fragment in characters. You can apply the * option to a specific field by passing the optional field name parameter. * * @example * const highlight = esb.highlight('content') * .fragmentSize(150, 'content') * .numberOfFragments(3, 'content'); * * @param {number} size The fragment size in characters. Defaults to 100. * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'fragmentSize', value: function fragmentSize(size, field) { this._setFieldOption(field, 'fragment_size', size); return this; } /** * Sets the maximum number of fragments to return. You can apply the * option to a specific field by passing the optional field name parameter. * * @example * const highlight = esb.highlight('content') * .fragmentSize(150, 'content') * .numberOfFragments(3, 'content'); * * @example * const highlight = esb.highlight(['_all', 'bio.title']) * .numberOfFragments(0, 'bio.title'); * * @param {number} count The maximum number of fragments to return * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'numberOfFragments', value: function numberOfFragments(count, field) { this._setFieldOption(field, 'number_of_fragments', count); return this; } /** * If `no_match_size` is set, in the case where there is no matching fragment * to highlight, a snippet of text, with the specified length, from the beginning * of the field will be returned. * * The actual length may be shorter than specified as it tries to break on a word boundary. * * Default is `0`. * * @example * const highlight = esb.highlight('content') * .fragmentSize(150, 'content') * .numberOfFragments(3, 'content') * .noMatchSize(150, 'content'); * * @param {number} size * @param {string} field * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'noMatchSize', value: function noMatchSize(size, field) { this._setFieldOption(field, 'no_match_size', size); return this; } /** * Highlight against a query other than the search query. * Useful if you use a rescore query because those * are not taken into account by highlighting by default. * * @example * const highlight = esb.highlight('content') * .fragmentSize(150, 'content') * .numberOfFragments(3, 'content') * .highlightQuery( * esb.boolQuery() * .must(esb.matchQuery('content', 'foo bar')) * .should( * esb.matchPhraseQuery('content', 'foo bar').slop(1).boost(10) * ) * .minimumShouldMatch(0), * 'content' * ); * * @param {Query} query * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained * @throws {TypeError} If `query` is not an instance of `Query` */ }, { key: 'highlightQuery', value: function highlightQuery(query, field) { checkType(query, Query); this._setFieldOption(field, 'highlight_query', query); return this; } /** * Combine matches on multiple fields to highlight a single field. * Useful for multifields that analyze the same string in different ways. * Sets the highlight type to Fast Vector Highlighter(`fvh`). * * @example * const highlight = esb.highlight('content') * .scoreOrder('content') * .matchedFields(['content', 'content.plain'], 'content'); * * highlight.toJSON(); * { * "order": "score", * "fields": { * "content": { * "matched_fields": ["content", "content.plain"], * "type" : "fvh" * } * } * } * * @param {Array<string>} fields * @param {string} field Field name * @returns {Highlight} returns `this` so that calls can be chained * @throws {Error} field parameter should be valid field name * @throws {TypeError} If `fields` is not an instance of Array */ }, { key: 'matchedFields', value: function matchedFields(fields, field) { checkType(fields, Array); if (isEmpty(field)) { throw new Error('`matched_fields` requires field name to be passed'); } this.type('fvh', field); this._setFieldOption(field, 'matched_fields', fields); return this; } /** * The fast vector highlighter has a phrase_limit parameter that prevents * it from analyzing too many phrases and eating tons of memory. It defaults * to 256 so only the first 256 matching phrases in the document scored * considered. You can raise the limit with the phrase_limit parameter. * * If using `matched_fields`, `phrase_limit` phrases per matched field * are considered. * * @param {number} limit Defaults to 256. * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'phraseLimit', value: function phraseLimit(limit) { this._highlight.phrase_limit = limit; return this; } /** * Can be used to define how highlighted text will be encoded. * * @param {string} encoder It can be either default (no encoding) * or `html` (will escape `html`, if you use html highlighting tags) * @returns {Highlight} returns `this` so that calls can be chained * @throws {Error} Encoder can be either `default` or `html` */ }, { key: 'encoder', value: function encoder(_encoder) { if (isNil(_encoder)) invalidEncoderParam(_encoder); var encoderLower = _encoder.toLowerCase(); if (encoderLower !== 'default' && encoderLower !== 'html') { invalidEncoderParam(_encoder); } this._highlight.encoder = encoderLower; return this; } /** * By default only fields that hold a query match will be highlighted. * This can be set to false to highlight the field regardless of whether * the query matched specifically on them. You can apply the * option to a specific field by passing the optional field name parameter. * * @example * const highlight = esb.highlight('_all') * .preTags('<em>', '_all') * .postTags('</em>', '_all') * .requireFieldMatch(false); * * @param {boolean} requireFieldMatch * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'requireFieldMatch', value: function requireFieldMatch(_requireFieldMatch, field) { this._setFieldOption(field, 'require_field_match', _requireFieldMatch); return this; } /** * Allows to control how far to look for boundary characters, and defaults to 20. * You can apply the option to a specific field by passing the optional field name parameter. * * @param {number} count The max characters to scan. * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'boundaryMaxScan', value: function boundaryMaxScan(count, field) { this._setFieldOption(field, 'boundary_max_scan', count); return this; } /** * Defines what constitutes a boundary for highlighting. * It is a single string with each boundary character defined in it. * It defaults to `.,!? \t\n`. You can apply the * option to a specific field by passing the optional field name parameter. * * @param {string} charStr * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'boundaryChars', value: function boundaryChars(charStr, field) { this._setFieldOption(field, 'boundary_chars', charStr); return this; } /** * Allows to force a specific highlighter type. * This is useful for instance when needing to use * the plain highlighter on a field that has term_vectors enabled. * You can apply the option to a specific field by passing the optional * field name parameter. * * Note: The `postings` highlighter has been removed in elasticsearch 6.0. * The `unified` highlighter outputs the same highlighting when * `index_options` is set to `offsets`. * * @example * const highlight = esb.highlight('content').type('plain', 'content'); * * @param {string} type The allowed values are: `plain`, `postings` and `fvh`. * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained * @throws {Error} Type can be one of `plain`, `postings` or `fvh`. */ }, { key: 'type', value: function type(_type, field) { if (isNil(_type)) invalidTypeParam(_type); var typeLower = _type.toLowerCase(); if (typeLower !== 'plain' && typeLower !== 'postings' && typeLower !== 'fvh') { invalidTypeParam(_type); } this._setFieldOption(field, 'type', typeLower); return this; } /** * Forces the highlighting to highlight fields based on the source * even if fields are stored separately. Defaults to false. * * @example * const highlight = esb.highlight('content').forceSource(true, 'content'); * * @param {boolean} forceSource * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained */ }, { key: 'forceSource', value: function forceSource(_forceSource, field) { this._setFieldOption(field, 'force_source', _forceSource); return this; } /** * Sets the fragmenter type. You can apply the * option to a specific field by passing the optional field name parameter. * Valid values for order are: * - `simple` - breaks text up into same-size fragments with no concerns * over spotting sentence boundaries. * - `span` - breaks text up into same-size fragments but does not split * up Spans. * * @example * const highlight = esb.highlight('message') * .fragmentSize(15, 'message') * .numberOfFragments(3, 'message') * .fragmenter('simple', 'message'); * * @param {string} fragmenter The fragmenter. * @param {string=} field An optional field name * @returns {Highlight} returns `this` so that calls can be chained * @throws {Error} Fragmenter can be either `simple` or `span` */ }, { key: 'fragmenter', value: function fragmenter(_fragmenter, field) { if (isNil(_fragmenter)) invalidFragmenterParam(_fragmenter); var fragmenterLower = _fragmenter.toLowerCase(); if (fragmenterLower !== 'simple' && fragmenterLower !== 'span') { invalidFragmenterParam(_fragmenter); } this._setFieldOption(field, 'fragmenter', fragmenterLower); return this; } // TODO: Support Explicit field order // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html#explicit-field-order /** * Override default `toJSON` to return DSL representation for the `highlight` request * * @override * @returns {Object} returns an Object which maps to the elasticsearch query DSL */ }, { key: 'toJSON', value: function toJSON() { return recursiveToJSON(this._highlight); } }]); return Highlight; }(); module.exports = Highlight;