Repository: searchbox-io/Jest
Pull request #273 (2015-12-09 18:01:14)

Add search templates
Added ability to search with templates (inline or stored) resolving #240.


Patch:
diff --git a/jest-common/src/main/java/io/searchbox/core/Search.java b/jest-common/src/main/java/io/searchbox/core/Search.java
index 9b2cae6c..7e3f9ed6 100644
--- a/jest-common/src/main/java/io/searchbox/core/Search.java
+++ b/jest-common/src/main/java/io/searchbox/core/Search.java
@@ -1,11 +1,13 @@
 package io.searchbox.core;
 
 import com.google.gson.Gson;
+
 import io.searchbox.action.AbstractAction;
 import io.searchbox.action.AbstractMultiTypeActionBuilder;
 import io.searchbox.core.search.sort.Sort;
 import io.searchbox.params.Parameters;
 import io.searchbox.params.SearchType;
+
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 
@@ -27,6 +29,15 @@ public class Search extends AbstractAction<SearchResult> {
         this.sortList = builder.sortList;
         setURI(buildURI());
     }
+    
+    protected Search(TemplateBuilder templatedBuilder) {
+        super(templatedBuilder);
+
+        //reuse query as it's just the request body of the POST
+        this.query = templatedBuilder.query;
+        this.sortList = templatedBuilder.sortList;
+        setURI(buildURI() + "/template");
+    }
 
     @Override
     public SearchResult createNewElasticSearchResult(String responseBody, int statusCode, String reasonPhrase, Gson gson) {
@@ -68,7 +79,7 @@ public class Search extends AbstractAction<SearchResult> {
                 sortMaps.add(sort.toMap());
             }
 
-            Map rootJson = gson.fromJson(query, Map.class);
+            Map<String, List<Map<String, Object>>> rootJson = gson.fromJson(query, Map.class);
             rootJson.put("sort", sortMaps);
             data = gson.toJson(rootJson);
         }
@@ -104,8 +115,12 @@ public class Search extends AbstractAction<SearchResult> {
     }
 
     public static class Builder extends AbstractMultiTypeActionBuilder<Search, Builder> {
-        private String query;
-        private List<Sort> sortList = new LinkedList<Sort>();
+        protected String query;
+        protected List<Sort> sortList = new LinkedList<Sort>();
+        
+        protected Builder() {
+        	
+        }
 
         public Builder(String query) {
             this.query = query;
@@ -130,4 +145,15 @@ public class Search extends AbstractAction<SearchResult> {
             return new Search(this);
         }
     }
+    
+    public static class TemplateBuilder extends Builder {    	
+    	public TemplateBuilder(String templatedQuery) {    		
+            super.query = templatedQuery;
+        }
+    	
+    	@Override
+        public Search build() {
+            return new Search(this);
+        }
+    }
 }
diff --git a/jest/README.md b/jest/README.md
index 030fe9a3..a8c17926 100644
--- a/jest/README.md
+++ b/jest/README.md
@@ -285,6 +285,25 @@ Search search = new Search.Builder(query)
 SearchResult result = client.execute(search);
 ```
 
+By template;
+``` java
+String query = "{\n" +
+            "    \"id\": \"myTemplateId\"," +
+            "    \"params\": {\n" +
+            "        \"query_string\" : \"search for this\"" +
+            "    }\n" +
+            "}";
+
+Search search = new Search.TemplateBuilder(query)
+                // multiple index or types can be added.
+                .addIndex("twitter")
+                .addIndex("tweet")
+                .build();
+
+SearchResult result = client.execute(search);
+```
+Also supports inline search templates and file-based templates.
+
 By using SearchSourceBuilder;
 
 ```java
diff --git a/jest/pom.xml b/jest/pom.xml
index d673f47c..aa4c0d11 100644
--- a/jest/pom.xml
+++ b/jest/pom.xml
@@ -145,6 +145,11 @@
             <groupId>org.codehaus.groovy</groupId>
             <artifactId>groovy-all</artifactId>
         </dependency>
+        
+        <dependency>
+            <groupId>com.github.spullara.mustache.java</groupId>
+            <artifactId>compiler</artifactId>
+        </dependency>
 
     </dependencies>
 
diff --git a/pom.xml b/pom.xml
index 3f6b698e..4a9e0dce 100755
--- a/pom.xml
+++ b/pom.xml
@@ -74,6 +74,7 @@
         <hamcrest.version>1.3</hamcrest.version>
         <randomizedtesting.version>2.1.16</randomizedtesting.version>
         <groovy.version>2.4.4</groovy.version>
+        <mustache.version>0.8.13</mustache.version>
 
         <httpcore.version>4.4.4</httpcore.version>
         <httpclient.version>4.5.1</httpclient.version>
@@ -348,6 +349,13 @@
                 <version>1.0.0-beta8</version>
                 <scope>test</scope>
             </dependency>
+        
+            <dependency>
+                <groupId>com.github.spullara.mustache.java</groupId>
+                <artifactId>compiler</artifactId>
+                <version>${mustache.version}</version>
+                <scope>test</scope>
+            </dependency>            
 
             <!-- required by groovy scripting support in elasticsearch -->
             <dependency>


Test patch:
diff --git a/jest-common/src/test/java/io/searchbox/core/SearchTest.java b/jest-common/src/test/java/io/searchbox/core/SearchTest.java
index a60dde3a..0fc389c2 100644
--- a/jest-common/src/test/java/io/searchbox/core/SearchTest.java
+++ b/jest-common/src/test/java/io/searchbox/core/SearchTest.java
@@ -1,15 +1,22 @@
 package io.searchbox.core;
 
-import com.google.gson.*;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertNotEquals;
 import io.searchbox.action.Action;
 import io.searchbox.core.search.sort.Sort;
 import io.searchbox.core.search.sort.Sort.Sorting;
-import org.junit.Test;
 
 import java.util.Arrays;
 
-import static junit.framework.Assert.*;
-import static org.junit.Assert.assertNotEquals;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
 
 /**
  * @author Dogukan Sonmez
@@ -68,6 +75,18 @@ public class SearchTest {
                 .build();
         assertEquals("twitter%2Csearchbox/tweet%2Cjest/_search", search.getURI());
     }
+    
+    @Test
+    public void getURIForTemplateWithoutIndexAndType() {
+        Action search = new Search.TemplateBuilder("").build();
+        assertEquals("_all/_search/template", search.getURI());
+    }
+    
+    @Test
+    public void getURIForTemplateWithIndexAndType() {
+        Action search = new Search.TemplateBuilder("").addIndex("twitter").addType("tweet").build();
+        assertEquals("twitter/tweet/_search/template", search.getURI());
+    }
 
     @Test
     public void sortTest() {
diff --git a/jest/src/test/java/io/searchbox/core/SearchTemplateIntegrationTest.java b/jest/src/test/java/io/searchbox/core/SearchTemplateIntegrationTest.java
new file mode 100755
index 00000000..43c878f4
--- /dev/null
+++ b/jest/src/test/java/io/searchbox/core/SearchTemplateIntegrationTest.java
@@ -0,0 +1,140 @@
+package io.searchbox.core;
+
+import io.searchbox.client.JestResult;
+import io.searchbox.common.AbstractIntegrationTest;
+import io.searchbox.core.SearchResult.Hit;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.test.ESIntegTestCase;
+import org.junit.Test;
+
+/**
+ * @author Bobby Hubbard
+ */
+@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.SUITE, numDataNodes = 1)
+public class SearchTemplateIntegrationTest extends AbstractIntegrationTest {
+
+    private static final String INDEX = "twitter";
+    private static final String TYPE = "tweet";
+
+    private static final String QUERY = 
+    		"{" +
+            "    \"id\": \"templateId\"," +
+            "    \"params\": {" +
+            "        \"user\" : \"kimchy1\"" +
+            "    }" +
+            "}";
+    
+    private static final String INLINE = 
+    		"{" +
+    		"    \"inline\": {" +
+            "    	\"query\": {" +
+            "    		\"term\": {" +
+            "       		\"user\" : \"{{user}}\"" +
+            "    		}" +
+            "    	}," +
+            "    	\"sort\": \"num\"" +
+            "	}," +	
+            "   \"params\": {" +
+            "        \"user\" : \"kimchy1\"" +
+            "    }" +
+            "}";
+    
+    private static final String TEMPLATE = 
+    		"{" +
+    		"    \"template\": {" +
+            "    	\"query\": {" +
+            "    		\"term\": {" +
+            "       		\"user\" : \"{{user}}\"" +
+            "    		}" +
+            "    	}," +
+            "    	\"sort\": \"num\"" +
+            "	}" +	
+            "}";
+    
+
+    @Test
+    public void searchWithValidQuery() throws IOException {
+        JestResult result = client.execute(new Search.TemplateBuilder(INLINE).build());
+        assertTrue(result.getErrorMessage(), result.isSucceeded());
+    }
+    
+    @Test
+    public void searchTemplateInlineScript() throws Exception { 
+    	    	
+    	assertTrue(client().index(new IndexRequest(INDEX, TYPE).source("{\"user\":\"kimchy1\",\"num\":1}").refresh(true)).actionGet().isCreated());
+    	assertTrue(client().index(new IndexRequest(INDEX, TYPE).source("{\"user\":\"abcdef\",\"num\":2}").refresh(true)).actionGet().isCreated());
+    	assertTrue(client().index(new IndexRequest(INDEX, TYPE).source("{\"user\":\"abcdef\",\"num\":3}").refresh(true)).actionGet().isCreated());
+
+        //template includes sort
+        SearchResult result = client.execute(new Search.TemplateBuilder(INLINE).build());
+        assertTrue(result.getErrorMessage(), result.isSucceeded());
+
+        List<SearchResult.Hit<Object, Void>> hits = result.getHits(Object.class);
+        assertEquals(1, hits.size());
+        Hit<Object, Void> hit0 = hits.get(0);
+        
+        //check user
+        assertEquals("kimchy1", ((Map)hit0.source).get("user"));
+    }
+
+    @Test
+    public void searchTemplateInlineScriptWithSort() throws Exception { 
+    	    	
+    	assertTrue(client().index(new IndexRequest(INDEX, TYPE).source("{\"user\":\"kimchy1\",\"num\":1}").refresh(true)).actionGet().isCreated());
+    	assertTrue(client().index(new IndexRequest(INDEX, TYPE).source("{\"user\":\"kimchy1\",\"num\":0}").refresh(true)).actionGet().isCreated());
+        assertTrue(client().index(new IndexRequest(INDEX, TYPE).source("{\"user\":\"\"}").refresh(true)).actionGet().isCreated());
+
+        //template includes sort
+        SearchResult result = client.execute(new Search.TemplateBuilder(INLINE).build());
+        assertTrue(result.getErrorMessage(), result.isSucceeded());
+
+        List<SearchResult.Hit<Object, Void>> hits = result.getHits(Object.class);
+        assertEquals(2, hits.size());
+        Hit<Object, Void> hit0 = hits.get(0);
+        Hit<Object, Void> hit1 = hits.get(1);
+        
+        //check sort
+        assertEquals(1, hit0.sort.size());
+        assertEquals("0", hit0.sort.get(0));
+        assertEquals(1, hit1.sort.size());
+        assertEquals("1", hit1.sort.get(0));
+        
+        //check user
+        assertEquals("kimchy1", ((Map)hit0.source).get("user"));
+        assertEquals("kimchy1", ((Map)hit1.source).get("user"));
+    }
+    
+    @Test
+    public void searchTemplateIdScriptWithSort() throws Exception { 
+    	assertTrue(client().index(new IndexRequest(".scripts", "mustache", "templateId").source(TEMPLATE).refresh(true)).actionGet().isCreated());    	
+    	assertTrue(client().index(new IndexRequest(INDEX, TYPE).source("{\"user\":\"kimchy1\",\"num\":1}").refresh(true)).actionGet().isCreated());
+    	assertTrue(client().index(new IndexRequest(INDEX, TYPE).source("{\"user\":\"kimchy1\",\"num\":0}").refresh(true)).actionGet().isCreated());
+        assertTrue(client().index(new IndexRequest(INDEX, TYPE).source("{\"user\":\"\"}").refresh(true)).actionGet().isCreated());
+
+        //template includes sort
+        SearchResult result = client.execute(new Search.TemplateBuilder(QUERY).build());
+        assertTrue(result.getErrorMessage(), result.isSucceeded());
+
+        List<SearchResult.Hit<Object, Void>> hits = result.getHits(Object.class);
+        assertEquals(2, hits.size());
+        Hit<Object, Void> hit0 = hits.get(0);
+        Hit<Object, Void> hit1 = hits.get(1);
+        
+        //check sort
+        assertEquals(1, hit0.sort.size());
+        assertEquals("0", hit0.sort.get(0));
+        assertEquals(1, hit1.sort.size());
+        assertEquals("1", hit1.sort.get(0));
+        
+        //check user
+        assertEquals("kimchy1", ((Map)hit0.source).get("user"));
+        assertEquals("kimchy1", ((Map)hit1.source).get("user"));
+    }	
+    
+    //TODO: Test file-based templates $ES_CONFIG/scripts/test.mustache  
+}