Advanced scripts using script engines

A ScriptEngine is a backend for implementing a scripting language. It may also be used to write scripts that need to use advanced internals of scripting. For example, a script that wants to use term frequencies while scoring.

The plugin documentation has more information on how to write a plugin so that Elasticsearch will properly load it. To register the ScriptEngine, your plugin should implement the ScriptPlugin interface and override the getScriptEngine(Settings settings) method.

The following is an example of a custom ScriptEngine which uses the language name expert_scripts. It implements a single script called pure_df which may be used as a search script to override each document’s score as the document frequency of a provided term.

  1. private static class MyExpertScriptEngine implements ScriptEngine {
  2. @Override
  3. public String getType() {
  4. return "expert_scripts";
  5. }
  6. @Override
  7. public <T> T compile(
  8. String scriptName,
  9. String scriptSource,
  10. ScriptContext<T> context,
  11. Map<String, String> params
  12. ) {
  13. if (context.equals(ScoreScript.CONTEXT) == false) {
  14. throw new IllegalArgumentException(getType()
  15. + " scripts cannot be used for context ["
  16. + context.name + "]");
  17. }
  18. // we use the script "source" as the script identifier
  19. if ("pure_df".equals(scriptSource)) {
  20. ScoreScript.Factory factory = new PureDfFactory();
  21. return context.factoryClazz.cast(factory);
  22. }
  23. throw new IllegalArgumentException("Unknown script name "
  24. + scriptSource);
  25. }
  26. @Override
  27. public void close() {
  28. // optionally close resources
  29. }
  30. @Override
  31. public Set<ScriptContext<?>> getSupportedContexts() {
  32. return Collections.singleton(ScoreScript.CONTEXT);
  33. }
  34. private static class PureDfFactory implements ScoreScript.Factory,
  35. ScriptFactory {
  36. @Override
  37. public boolean isResultDeterministic() {
  38. // PureDfLeafFactory only uses deterministic APIs, this
  39. // implies the results are cacheable.
  40. return true;
  41. }
  42. @Override
  43. public LeafFactory newFactory(
  44. Map<String, Object> params,
  45. SearchLookup lookup
  46. ) {
  47. return new PureDfLeafFactory(params, lookup);
  48. }
  49. }
  50. private static class PureDfLeafFactory implements LeafFactory {
  51. private final Map<String, Object> params;
  52. private final SearchLookup lookup;
  53. private final String field;
  54. private final String term;
  55. private PureDfLeafFactory(
  56. Map<String, Object> params, SearchLookup lookup) {
  57. if (params.containsKey("field") == false) {
  58. throw new IllegalArgumentException(
  59. "Missing parameter [field]");
  60. }
  61. if (params.containsKey("term") == false) {
  62. throw new IllegalArgumentException(
  63. "Missing parameter [term]");
  64. }
  65. this.params = params;
  66. this.lookup = lookup;
  67. field = params.get("field").toString();
  68. term = params.get("term").toString();
  69. }
  70. @Override
  71. public boolean needs_score() {
  72. return false; // Return true if the script needs the score
  73. }
  74. @Override
  75. public ScoreScript newInstance(LeafReaderContext context)
  76. throws IOException {
  77. PostingsEnum postings = context.reader().postings(
  78. new Term(field, term));
  79. if (postings == null) {
  80. /*
  81. * the field and/or term don't exist in this segment,
  82. * so always return 0
  83. */
  84. return new ScoreScript(params, lookup, context) {
  85. @Override
  86. public double execute(
  87. ExplanationHolder explanation
  88. ) {
  89. return 0.0d;
  90. }
  91. };
  92. }
  93. return new ScoreScript(params, lookup, context) {
  94. int currentDocid = -1;
  95. @Override
  96. public void setDocument(int docid) {
  97. /*
  98. * advance has undefined behavior calling with
  99. * a docid <= its current docid
  100. */
  101. if (postings.docID() < docid) {
  102. try {
  103. postings.advance(docid);
  104. } catch (IOException e) {
  105. throw new UncheckedIOException(e);
  106. }
  107. }
  108. currentDocid = docid;
  109. }
  110. @Override
  111. public double execute(ExplanationHolder explanation) {
  112. if (postings.docID() != currentDocid) {
  113. /*
  114. * advance moved past the current doc, so this
  115. * doc has no occurrences of the term
  116. */
  117. return 0.0d;
  118. }
  119. try {
  120. return postings.freq();
  121. } catch (IOException e) {
  122. throw new UncheckedIOException(e);
  123. }
  124. }
  125. };
  126. }
  127. }
  128. }

You can execute the script by specifying its lang as expert_scripts, and the name of the script as the script source:

  1. POST /_search
  2. {
  3. "query": {
  4. "function_score": {
  5. "query": {
  6. "match": {
  7. "body": "foo"
  8. }
  9. },
  10. "functions": [
  11. {
  12. "script_score": {
  13. "script": {
  14. "source": "pure_df",
  15. "lang" : "expert_scripts",
  16. "params": {
  17. "field": "body",
  18. "term": "foo"
  19. }
  20. }
  21. }
  22. }
  23. ]
  24. }
  25. }
  26. }