Skip to content

Commit

Permalink
Add RuntimeField, esb.runtimeField, esb.requestBodySearch().runtimeFi…
Browse files Browse the repository at this point in the history
…eld() and esb.requestBodySearch().runtimeFields()
  • Loading branch information
jmaitrehenry authored and atreids committed Feb 28, 2024
1 parent 8002c89 commit 9f0e0fc
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"author": "Suhas Karanth <sudo.suhas@gmail.com>",
"contributors": [
"austin ce <austin.cawley@gmail.com>",
"Julien Maitrehenry <julien.maitrehenry@me.com>",
"ochan12 <mateochando@gmail.com>",
"kennylindahl <haxxblaster@gmail.com>",
"foxstarius <aj.franzon@gmail.com>",
Expand Down
2 changes: 2 additions & 0 deletions src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ exports.SearchTemplate = require('./search-template');
exports.consts = require('./consts');

exports.util = require('./util');

exports.RuntimeField = require('./runtime-field');
68 changes: 68 additions & 0 deletions src/core/request-body-search.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,74 @@ class RequestBodySearch {
return this;
}

/**
* Computes a document property dynamically based on the supplied `script`.
*
* @example
* const reqBody = esb.requestBodySearch()
* .query(esb.matchAllQuery())
* .runtimeField(
* 'sessionId-name',
* esb.runtimeField(
* 'keyword',
* `emit(doc['session_id'].value + '::' + doc['name'].value)`
* )
* )
*
* @example
* // runtime fields can also be used in query aggregation
* const reqBody = esb.requestBodySearch()
* .query(esb.matchAllQuery())
* .runtimeField(
* 'sessionId-eventName',
* esb.runtimeField(
* 'keyword',
* `emit(doc['session_id'].value + '::' + doc['eventName'].value)`,
* )
* )
* .agg(esb.cardinalityAggregation('uniqueCount', `sessionId-eventName`)),;
*
* @param {string} runtimeFieldName
* @param {RuntimeField} instance of `RuntimeField`
* @returns {RequestBodySearch} returns `this` so that calls can be chained
*/
runtimeMapping(runtimeMappingName, runtimeField) {
setDefault(this._body, 'runtime_mappings', {});
this._body.runtime_mappings[runtimeMappingName] = runtimeField;
return this;
}

/**
* Computes one or more document properties dynamically based on supplied `RuntimeField`s.
*
* @example
* const fieldA = esb.runtimeField(
* 'keyword',
* `emit(doc['session_id'].value + '::' + doc['name'].value)`,
* 'sessionId-name'
* );
* const reqBody = esb.requestBodySearch()
* .query(esb.matchAllQuery())
* .runtimeFields({
* 'sessionId-name': fieldA,
* })
*
* @param {Object} runtimeFields Object with `runtimeFieldName` as key and `RuntimeField` instance as the value.
* @returns {RequestBodySearch} returns `this` so that calls can be chained
*/
runtimeMappings(runtimeMappings) {
checkType(runtimeMappings, Object);

Object.keys(runtimeMappings).forEach(runtimeMappingName =>
this.runtimeMapping(
runtimeMappingName,
runtimeMappings[runtimeMappingName]
)
);

return this;
}

/**
* Computes a document property dynamically based on the supplied `Script`.
*
Expand Down
71 changes: 71 additions & 0 deletions src/core/runtime-field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use strict';

const isNil = require('lodash.isnil');
const validType = [
'boolean',
'composite',
'date',
'double',
'geo_point',
'ip',
'keyword',
'long',
'lookup'
];

class RuntimeField {
constructor(type, script, name) {
this._body = {};
this._name = name;
this._isTypeSet = false;
this._isScriptSet = false;

if (!isNil(type)) {
this.type(type);
}

if (!isNil(script)) {
this.script(script);
}
}

name(name) {
this._name = name;
}

script(script) {
this._body.script = {
source: script
};
this._isScriptSet = true;
}

type(type) {
const typeLower = type.toLowerCase();
if (!validType.includes(typeLower)) {
throw new Error(`\`type\` must be one of ${validType.join(', ')}`);
}
this._body.type = typeLower;
this._isTypeSet = true;
}

/**
* Override default `toJSON` to return DSL representation for the `script`.
*
* @override
* @returns {Object} returns an Object which maps to the elasticsearch query DSL
*/
toJSON() {
if (!this._isTypeSet) {
throw new Error('`type` should be set');
}

if (!this._isScriptSet) {
throw new Error('`script` should be set');
}

return this._body;
}
}

module.exports = RuntimeField;
17 changes: 17 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ declare namespace esb {
*/
storedFields(fields: object | string): this;

runtimeMapping(runtimeMappingName: string, script: string | RuntimeField): this;
runtimeMappings(runtimeMappings: object): this;

/**
* Computes a document property dynamically based on the supplied `Script`.
*
Expand Down Expand Up @@ -8761,6 +8764,20 @@ declare namespace esb {
*/
export function highlight(fields?: string | string[]): Highlight;

export class RuntimeField {
constructor(type?: string, script?: string, name?: string);
name(name: string);
type(type: 'boolean' | 'composite' | 'date' | 'double' | 'geo_point' | 'ip' | 'keyword' | 'long' | 'lookup');
script(script: string);
/**
* Override default `toJSON` to return DSL representation for the `script`.
*
* @override
*/
toJSON(): object;
}
export function runtimeField(type?: string, script?: string, name?: string): RuntimeField;

/**
* Class supporting the Elasticsearch scripting API.
*
Expand Down
4 changes: 4 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
Sort,
Rescore,
InnerHits,
RuntimeField,
SearchTemplate,
Query,
util: { constructorWrapper }
Expand Down Expand Up @@ -640,6 +641,9 @@ exports.innerHits = constructorWrapper(InnerHits);
exports.SearchTemplate = SearchTemplate;
exports.searchTemplate = constructorWrapper(SearchTemplate);

exports.RuntimeField = RuntimeField;
exports.runtimeField = constructorWrapper(RuntimeField);

exports.prettyPrint = function prettyPrint(obj) {
console.log(JSON.stringify(obj, null, 2));
};
Expand Down
47 changes: 46 additions & 1 deletion test/core-test/request-body-search.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import test from 'ava';
import {
RequestBodySearch,
Expand All @@ -14,7 +15,8 @@ import {
Script,
Highlight,
Rescore,
InnerHits
InnerHits,
RuntimeField
} from '../../src';
import { illegalParamType, makeSetsOptionMacro } from '../_macros';

Expand All @@ -37,6 +39,17 @@ const suggest = new TermSuggester(
const sortChannel = new Sort('channel', 'desc');
const sortCategories = new Sort('categories', 'desc');

const runtimeFieldA = new RuntimeField(
'keyword',
"emit(doc['name'].value)",
'test1'
);
const runtimeFieldB = new RuntimeField(
'boolean',
"emit(doc['qty'].value > 10)",
'test2'
);

const scriptA = new Script('inline', "doc['my_field_name'].value * 2").lang(
'painless'
);
Expand Down Expand Up @@ -131,6 +144,38 @@ test('sets stored_fields(arr) option', setsOption, 'storedFields', {
param: ['user', 'postDate'],
spread: false
});

test(setsOption, 'runtimeMapping', {
param: ['test1', runtimeFieldA],
propValue: {
test1: {
type: 'keyword',
script: {
source: "emit(doc['name'].value)"
}
}
},
keyName: 'runtime_mappings'
});
test(setsOption, 'runtimeMappings', {
param: { test1: runtimeFieldA, test2: runtimeFieldB },
propValue: {
test1: {
type: 'keyword',
script: {
source: "emit(doc['name'].value)"
}
},
test2: {
type: 'boolean',
script: {
source: "emit(doc['qty'].value > 10)"
}
}
},
keyName: 'runtime_mappings'
});

test(setsOption, 'scriptField', {
param: ['test1', scriptA],
propValue: { test1: { script: scriptA } },
Expand Down
52 changes: 52 additions & 0 deletions test/core-test/runtime-field.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import test from 'ava';
import RuntimeField from '../../src/core/runtime-field';

test('constructor set arguments', t => {
const valueA = new RuntimeField(
'keyword',
"emit(doc['name'].value)"
).toJSON();
const valueB = new RuntimeField(
'keyword',
"emit(doc['name'].value)"
).toJSON();
t.deepEqual(valueA, valueB);

const expected = {
type: 'keyword',
script: {
source: "emit(doc['name'].value)"
}
};
t.deepEqual(valueA, expected);

let err = t.throws(() => new RuntimeField().toJSON(), Error);
t.is(err.message, '`type` should be set');

err = t.throws(() => new RuntimeField('keyword').toJSON(), Error);
t.is(err.message, '`script` should be set');
});

test('type validate and set argument', t => {
const fieldA = new RuntimeField('keyword', "emit(doc['name'].value)");
fieldA.type('boolean');
const expected = {
type: 'boolean',
script: {
source: "emit(doc['name'].value)"
}
};
t.deepEqual(fieldA.toJSON(), expected);

const err = t.throws(() => fieldA.type('invalid'), Error);
t.is(
err.message,
'`type` must be one of boolean, composite, date, double, geo_point, ip, keyword, long, lookup'
);
});

test('name set _name', t => {
const fieldA = new RuntimeField();
fieldA.name('field-name');
t.deepEqual(fieldA._name, 'field-name');
});

0 comments on commit 9f0e0fc

Please sign in to comment.