es官方文档地址:https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-search.html
一、需求:
1、Entry写入ES数据
2、根据参数值更改ES数据
3、新增存储信息接口
4、删除存储信息接口
5、根据id获取ES数据
6、sql语句转为es查询语句
7、模板查询(最后附上查询模板)
二、pom.xml
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.7.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.nlpcn/elasticsearch-sql -->
<dependency>
<groupId>org.nlpcn</groupId>
<artifactId>elasticsearch-sql</artifactId>
<version>6.8.13.0</version>
</dependency>
三、centroller层
package filemanager
package metastorage
package impl
import java.io.{ StringReader, StringWriter }
import java.util.{ List => JList, Map => JMap, HashMap => JHashMap }
import persimmon.ngps.designpattern._
import persimmon.ngps.structured._
import persimmon.ngps.structured.reader.StructuredReaders
import persimmon.ngps.structured.jackson2._
import persimmon.ngps.metadata._
import persimmon.ngps.config._
import persimmon.ngps.persistence.orm.SeqWithTotal
import persimmon.ngps.log.LoggerUtils
import filemanager.commons.FileAttributes
import filemanager.metastorage.impl.ESUtils
/** 元数据存储访问模块实现 */
@summary("元数据存储访问模块实现")
@configEntries
object MetaStorageImplementation
extends MetaStorage
with FileManager.Module {
//用于更新元数据存储实体类
@facade @caseclass @partialIncludes @setterIncludes
@customizedStructure
trait UpdateMetadata {
@field
def attributes: FileAttributes
@field
def metadata: JMap[String, AnyRef]
@field
def lastModified: Long
@field
def lastAccessed: Long
}
//用于增加元数据StorageEntry实体类
@facade @caseclass @partialIncludes @setterIncludes
@customizedStructure
trait AddStoragedata {
@field
def storage: Map[String, StorageEntryImplementation]
@field
def lastModified: Long
@field
def lastAccessed: Long
}
/** Elasticsearch主机地址 */
@requiredConfig @summary("Elasticsearch主机地址")
val elasticsearchHost: String = ""
/** Elasticsearch端口号 */
@optionalConfig @summary("Elasticsearch端口号")
val elasticsearchPort: Int = 19200
/** Elasticsearch端口号 */
@requiredConfig @summary("Elasticsearch表名")
val elasticsearchIndex: String = ""
/** Elasticsearch.clustername */
@requiredConfig @summary("Elasticsearchclustername")
val elasticsearchClusterName: String = ""
initializeConfigPackage
//此方法为 根据传过来的参数值,添加到ES数据库中
override def addMetadata(id: String, attributes: FileAttributes, storage: Seq[StorageEntry], metadata: JMap[String, AnyRef]) {
LoggerUtils logInfo ("新增元数据(addMetadata)", id, attributes, storage, metadata)
//获取当前时间戳
val time = System.currentTimeMillis
//MetaEntryImplementation为实体类
val metaEntry = MetaEntryImplementation(
attributes = attributes,
storageDictionary = storage map StorageEntryImplementation.fromStorageEntry,
metadata = metadata,
created = time,
lastModified = time,
lastAccessed = time)
//Entry 转为 String
val metaEntryString = JacksonStructuredFormatterFactory.Json writeAsText metaEntry
//调用ESUtils中的方法添加数据
val client = ESUtils.getRestHighLevelClient(elasticsearchHost, elasticsearchPort)
val response = ESUtils.bulkInsert(elasticsearchIndex, id, metaEntryString, client)
val status = response.status().getStatus
println(status)
client.close()
//println(metaEntryString)
//String 转为 Entry
//val metaEntryS= JacksonStructuredParserFactory.Json readFromText[MetaEntry] metaEntryString
LoggerUtils logInfo ("新增元数据(addMetadata)=========" + id + "======成功!")
}
//根据传过来的参数值,进行update
override def updateMetadata(id: String, attributes: FileAttributes, metadata: JMap[String, AnyRef]) {
LoggerUtils logInfo ("更新元数据(updateMetadata)", id, attributes, metadata)
//获取当前时间戳
val time = System.currentTimeMillis
val updateMetadata = UpdateMetadata(
attributes = attributes,
metadata = metadata,
lastModified = time,
lastAccessed = time)
//Entry 转为 String
val updateMetadataToString = JacksonStructuredFormatterFactory.Json writeAsText updateMetadata
val client = ESUtils.getRestHighLevelClient(elasticsearchHost, elasticsearchPort)
val response = ESUtils.updateESData(elasticsearchIndex, id, updateMetadataToString, client)
client.close()
}
}
//根据addStorage 新增存储信息接口
override def addStorage(id: String, storage: StorageEntry) {
LoggerUtils logInfo ("新增存储信息(addStorage)", id, storage)
//获取当前时间戳
val time = System.currentTimeMillis
val addMetadata = AddStoragedata(
storage = Map(StorageEntryImplementation fromStorageEntry storage),
lastModified = time,
lastAccessed = time)
val addMetadataToString = JacksonStructuredFormatterFactory.Json writeAsText addMetadata
val client = ESUtils.getRestHighLevelClient(elasticsearchHost, elasticsearchPort)
val response = ESUtils.updateESData(elasticsearchIndex, id, addMetadataToString, client)
client.close()
}
}
//删除存储信息接口
override def removeStorage(id: String, filesystem: String) {
LoggerUtils logInfo ("删除存储信息(removeStorage)", id, filesystem)
/*val filesystemString = s"""
{"storage":
{"$filesystem":
{
"state": "pending"
}
}
}
""" */
//获取当前时间戳
val time = System.currentTimeMillis
val updateMetadata : JMap[String, AnyRef] = new JHashMap[String, AnyRef]
val storageMap = new JHashMap[String, AnyRef]
val filesystemMap = new JHashMap[String, String]
filesystemMap put ("state", StorageEntry.DisabledState)
storageMap put (filesystem, filesystemMap)
updateMetadata put ("lastAccessed", Long box time)
updateMetadata put ("lastModified", Long box time)
updateMetadata put ("storage", storageMap)
println(updateMetadata)
import writer.StructuredWriters._
val addMetadataToString = JacksonStructuredFormatterFactory.Json.writeAsText(updateMetadata)
val client = ESUtils.getRestHighLevelClient(elasticsearchHost, elasticsearchPort)
val response = ESUtils.updateESData(elasticsearchIndex, id, addMetadataToString, client)
val status = response.status().getStatus
println("删除数据返回状态============="+ status)
client.close()
}
}
//根据id获取es数据
override def getMetadata(id: String): Option[MetaEntry] = {
LoggerUtils logInfo ("获取元数据(getMetadata)", id)
val client = ESUtils.getRestHighLevelClient(elasticsearchHost, elasticsearchPort)
val response = ESUtils.getESdataById(elasticsearchIndex, id, client)
if(response.isExists()){
val responseString = response.getSourceAsString
println("--impl返回值----"+ responseString)
val metaEntryImplementation= JacksonStructuredParserFactory.Json readFromText[MetaEntryImplementation] responseString
LoggerUtils logInfo ("获取元数据=====" + elasticsearchIndex + "==为空======获取成功!")
client.close()
Some(metaEntryImplementation)
}else{
LoggerUtils logInfo ("获取元数据=====" + elasticsearchIndex + "==为空======获取失败!")
client.close()
None
}
}
//sql语句转为es查询语句
override def transformSqlWhereClause(whereClause : String) : JMap[String, AnyRef] = {
LoggerUtils logInfo ("sql语句查询ES", whereClause)
//whereClause只是where后面的条件 ,select需要自己添加
val sql = "select * from filemetadata where " + whereClause
//将sql转为es查询条件
val esString = ESUtils.sqlToEsQuery(sql)
println("此为sql转为es查询语句==="+esString)
//将String转为Map
import StructuredReaders._
val esMap : JMap[String, AnyRef] = JacksonStructuredParserFactory.Json.readFromText[JMap[String, AnyRef]](esString)
val queryMap = esMap.get("query").asInstanceOf[JMap[String, AnyRef]]
val result = new JHashMap[String, AnyRef]
result.put("customized", queryMap)
return result
}
// 模板查询
override def query(
query: JMap[String, AnyRef],
sortField: Option[String],
isAscending: Boolean,
after: Int Either JList[AnyRef],
size: Int): SeqWithTotal[MetaEntry] = {
LoggerUtils logInfo ("条件查询(query)", query, sortField, isAscending, after, size)
println("条件查询(query)", query, sortField, isAscending, after, size)
SeqWithTotal[MetaEntry](elements = Nil, total = 0L)
val client = ESUtils.getRestHighLevelClient(elasticsearchHost, elasticsearchPort)
//实现模板查询
after match {
case Left(from) =>
val response = ESUtils.getESdataByFromTemplate(elasticsearchIndex,
client, query, sortField,
isAscending, from, size)
val getHits = response.getResponse.getHits
val hit = getHits.getHits
val elements= hit.map(
x => {
val MetaEntrystring = x.getSourceAsString
println(MetaEntrystring)
val metaEntryImplementation= JacksonStructuredParserFactory.Json readFromText[MetaEntryImplementation] MetaEntrystring
MetaEntryImplementation.copy(metaEntryImplementation)(idOption = Some(x.getId))
})
SeqWithTotal.apply(elements, hit.length)
case Right(searchAfter) =>
val response = ESUtils.getESdataBySearchAfterTemplate(elasticsearchIndex, client, query, sortField, isAscending, searchAfter, size)
val getHits = response.getResponse.getHits
val hit = getHits.getHits
val elements= hit.map(
x => {
val MetaEntrystring = x.getSourceAsString
println(MetaEntrystring)
val metaEntryImplementation= JacksonStructuredParserFactory.Json readFromText[MetaEntryImplementation] MetaEntrystring
MetaEntryImplementation.copy(metaEntryImplementation)(idOption = Some(x.getId))
})
SeqWithTotal.apply(elements, hit.length)
}
}
}
四、ESUtils—实现层
package filemanager.metastorage.impl
import org.elasticsearch.client.RestHighLevelClient
import org.apache.http.HttpHost
import org.elasticsearch.client.RestClient
import org.elasticsearch.action.index.IndexRequest
import org.elasticsearch.common.xcontent.XContentType
import org.elasticsearch.client.RequestOptions
import org.elasticsearch.action.update.UpdateRequest
import org.elasticsearch.common.unit.TimeValue
import org.elasticsearch.action.update.UpdateResponse
import org.elasticsearch.action.get.GetRequest
import org.elasticsearch.action.get.GetResponse
import java.util.{ List => JList, Map => JMap, HashMap => JHashMap }
import org.elasticsearch.action.search.SearchRequest
import com.sun.xml.internal.ws.client.sei.ResponseBuilder.SourceBuilder
import org.elasticsearch.search.builder.SearchSourceBuilder
import org.elasticsearch.index.query.QueryBuilders
import org.elasticsearch.action.search.SearchResponse
import org.elasticsearch.search.sort.SortOrder
import org.elasticsearch.script.mustache.SearchTemplateRequest
import org.elasticsearch.script.ScriptType
import org.elasticsearch.script.mustache.SearchTemplateResponse
object ESUtils {
//获取searchDao ,用于sql转为es查询语句命令
lazy val searchDao = {
import org.elasticsearch.client.Client
import org.elasticsearch.client.node.NodeClient
import org.elasticsearch.common.settings.Settings
import org.elasticsearch.common.util.concurrent.ThreadContext
import org.elasticsearch.threadpool.ThreadPool
val settings = Settings.builder().put("cluster.name", MetaStorageImplementation.elasticsearchClusterName).build()
//val settings = Settings.builder().put("cluster.name", "my-application").build()
val threadPool = new ThreadPool(settings)
val client = new NodeClient(settings, threadPool)
new org.nlpcn.es4sql.SearchDao(client)
}
// 获取ES连接
def getRestHighLevelClient(host: String, port: Int): RestHighLevelClient = {
val client = new RestHighLevelClient(RestClient.builder(new HttpHost(host, port, "http")))
return client
}
// 关闭ES连接
def closeESClient(client: RestHighLevelClient) {
try {
client.close()
} catch {
case e: Exception =>
e.printStackTrace()
}
}
// 使用bulk插入ES
def bulkInsert(indexName: String, id: String, value: String, client: RestHighLevelClient) = {
val req = new IndexRequest(indexName, "_doc", id)
req.source(value, XContentType.JSON)
println(s"$id, $value")
val response = client.index(req, RequestOptions.DEFAULT)
println("状态=============" + response.status.getStatus)
//println(response)
response
}
// update ES data
def updateESData(indexName: String, id: String, value: String, client: RestHighLevelClient): UpdateResponse = {
val request = new UpdateRequest(indexName, "_doc", id).doc(value, XContentType.JSON)
//request.timeout("10s");
val response = client.update(request, RequestOptions.DEFAULT)
response
}
/*//delete ES data
def removeByIdAndFilesystem(indexName: String, id: String, jsonMap: JMap[String, AnyRef], client: RestHighLevelClient){
val request = new UpdateRequest(indexName, "_doc", id).doc(jsonMap)
val response = client.update(request, RequestOptions.DEFAULT)
response
}*/
// get ES data by id
def getESdataById(indexName: String, id: String, client: RestHighLevelClient): GetResponse = {
val getRequest = new GetRequest(indexName, "_doc", id)
val getResponse = client.get(getRequest, RequestOptions.DEFAULT)
getResponse
}
//search from 模板查询
def getESdataByFromTemplate(
indexName: String,
client: RestHighLevelClient,
query: JMap[String, AnyRef],
sortField: Option[String],
isAscending: Boolean,
from: Int,
size: Int) : SearchTemplateResponse = {
val request = new SearchTemplateRequest()
request.setRequest(new SearchRequest(indexName));
request.setScriptType(ScriptType.STORED)
request.setScript("filemetadata")
//最外层map
//val map = new JHashMap[String, AnyRef]
//term -map
//val params = new JHashMap[String, AnyRef]
//lastAccessed map
//val param = new JHashMap[String, AnyRef]
//params.put("term", query)
//param.put("start", Long.box(1648540192929L))
//map.put("customized", params)
//query.put("lastAccessed", param)
query.put("from", Int.box(from))
if (size == 0) {
query.put("size", Int.box(10))
} else {
query.put("size", Int.box(size))
}
if (sortField.isDefined) {
if (isAscending) {
query.put("asc", sortField.get)
} else {
query.put("desc", sortField.get)
}
}
request.setScriptParams(query)
val response = client.searchTemplate(request, RequestOptions.DEFAULT)
val getHits = response.getResponse.getHits
val hit = getHits.getHits
println(hit.length)
/*hit.map(
x => {
println(x)
})*/
client.close()
response
}
//searchAfter查询
def getESdataBySearchAfterTemplate(
indexName: String,
client: RestHighLevelClient,
query: JMap[String, AnyRef],
sortField: Option[String],
isAscending: Boolean,
from: JList[AnyRef],
size: Int) : SearchTemplateResponse = {
val request = new SearchTemplateRequest()
request.setRequest(new SearchRequest(indexName));
request.setScriptType(ScriptType.STORED)
request.setScript("filemetadata")
//最外层map
//val map = new JHashMap[String, AnyRef]
//term -map
//val params = new JHashMap[String, AnyRef]
//lastAccessed map
//val param = new JHashMap[String, AnyRef]
//params.put("term", query)
//param.put("start", Long.box(1648540192929L))
//map.put("customized", params)
//query.put("lastAccessed", param)
query.put("search_after", from)
if (size == 0) {
query.put("size", Int.box(10))
} else {
query.put("size", Int.box(size))
}
if (sortField.isDefined) {
if (isAscending) {
query.put("asc", sortField.get)
} else {
query.put("desc", sortField.get)
}
}
request.setScriptParams(query)
val response = client.searchTemplate(request, RequestOptions.DEFAULT)
val getHits = response.getResponse.getHits
val hit = getHits.getHits
println(hit.length)
/*hit.map(
x => {
println(x)
})*/
client.close()
response
}
//通过sql转为es查询语句
def sqlToEsQuery(sql : String) : String = {
val string = searchDao.explain(sql).explain().explain()
string
}
//get ES data 多层嵌套查询
def getESData(query: JMap[String, AnyRef],
indexName: String,
sortField: Option[String],
client: RestHighLevelClient,
isAscending : Boolean,
//after: Int Either JList[AnyRef],
size: Int
): SearchResponse = {
var asc = SortOrder.ASC
val request = new SearchRequest(indexName)
val searchSourceBuilder = new SearchSourceBuilder()
val boolQuery = QueryBuilders.boolQuery()
boolQuery.filter().add(QueryBuilders.rangeQuery("lastAccessed").gte(query.get("lastAccessed")))
searchSourceBuilder.query(boolQuery)
if(!sortField.isEmpty){
if(isAscending){
asc = SortOrder.ASC
}else{
asc = SortOrder.DESC
}
searchSourceBuilder.sort(sortField.get, asc)
}else{
//searchSourceBuilder.searchAfter(x$1)
if (size != 0) {
searchSourceBuilder.size(size);
}
}
request.source(searchSourceBuilder)
val response = client.search(request, RequestOptions.DEFAULT)
response
}
}
五、test类,用于测试ESUtils方法
package filemanager
package metastorage
package impl
import filemanager.commons.FileAttributes
import java.util.{ List => JList, Map => JMap, HashMap => JHashMap }
import org.elasticsearch.script.mustache.SearchTemplateRequest
import org.elasticsearch.action.search.SearchRequest
import org.elasticsearch.script.ScriptType
import org.elasticsearch.search.builder.SearchSourceBuilder
import org.elasticsearch.index.query.QueryBuilders
import persimmon.ngps.structured._
import persimmon.ngps.structured.reader.StructuredReaders
import persimmon.ngps.structured.jackson2._
import org.elasticsearch.client.RequestOptions
import java.nio.file.Files
import java.io.File
import java.nio.charset.StandardCharsets
import org.elasticsearch.action.explain.ExplainAction
import org.apache.lucene.search.Explanation
//import org.elasticsearch.transport.client.PreBuiltTransportClient
import org.elasticsearch.action.index.IndexRequest
import org.elasticsearch.common.xcontent.XContentType
import org.elasticsearch.action.get.GetRequest
import java.util.ArrayList
object test {
private val client = ESUtils.getRestHighLevelClient("192.168.12.148", 19200)
//测试根据id获取数据
def getMetadata(){
val id = "02130bbaf56b48e99752c406606795d5"
val entry = MetaStorageImplementation.getMetadata(id)
println(entry)
}
def delete(){
//测试根据id、filesystem删除数据
val id = "ce0667ec4e284683970364e689f09f28"
val filesystem = "hdfs"
MetaStorageImplementation.removeStorage(id, filesystem)
}
//测试更新接口
def updateMetadata(){
val myMap: Map[String, Object] = Map("key1" -> "testUpdate",
"key2" -> "100"
)
val a : Option[String] = myMap.get("key1").asInstanceOf[Option[String]]
val c = Option.apply(5L)
val time = System.currentTimeMillis()
val id = "ce0667ec4e284683970364e689f09f28"
val attributes = FileAttributes(
"testUpdate",
a,
100L,
a,
c,
time
)
val metadata : JMap[String, AnyRef] = new JHashMap[String, AnyRef]
metadata.put("description", "test01Updata")
MetaStorageImplementation.updateMetadata(id, attributes, metadata)
}
def addStorage(){
val id = "afb7a15775504f5596051e8e4446877e"
val storage = StorageEntry(
"persistence",
"我是path",
"disabled"
)
MetaStorageImplementation.addStorage(id , storage)
}
def searchTemplate(){
val request = new SearchTemplateRequest()
request.setRequest(new SearchRequest("filemetadata"));
request.setScriptType(ScriptType.STORED)
request.setScript("filemetadata")
val map = new JHashMap[String, AnyRef]
val param = new JHashMap[String, AnyRef]
val params = new JHashMap[String, AnyRef]
param.put("lastAccessed", Long box 1648194896310L)
params.put("term", param)
map.put("customized", params)
request.setScriptParams(map)
val response = client.searchTemplate(request, RequestOptions.DEFAULT)
val getHits = response.getResponse.getHits
val hit = getHits.getHits
hit.foreach(
x=>{
println(x)
}
)
}
def search(){
/*val request = new SearchRequest("filemetadata")
val builder = new SearchSourceBuilder()
builder.query(QueryBuilders.rangeQuery("lastAccessed").lte(1))
*/
val myMap: Map[String, Object] = Map("key1" -> "lastAccessed",
"key2" -> "100"
)
val a : Option[String] = myMap.get("key1").asInstanceOf[Option[String]]
val query : JMap[String, AnyRef] = new JHashMap[String, AnyRef]
query.put("lastAccessed", Long box 1648194896312L)
/*val re = ESUtils.getESData(query, "filemetadata", a, client, false , 3)
re.getHits.getHits.foreach(x=>{
println(x.getSourceAsString)
})*/
client.close()
}
def search01(){
val query = new JHashMap[String, AnyRef]
val param = new JHashMap[String, AnyRef]
param.put("start", Long.box(1648540192929L))
query.put("attributes.name", "test0331")
query.put("lastAccessed",param)
val myMap: Map[String, Object] = Map("key1" -> "lastAccessed")
val sortField = myMap.get("key1").asInstanceOf[Option[String]]
val isAscending = false
val from = 0
val size = 0
ESUtils.getESdataByFromTemplate("filemetadata", client, query, sortField, isAscending, from, size)
}
def get(sql : String) : String ={
import org.elasticsearch.client.Client
import org.elasticsearch.client.node.NodeClient
import org.elasticsearch.common.settings.Settings
import org.elasticsearch.common.util.concurrent.ThreadContext
import org.elasticsearch.threadpool.ThreadPool
val settings = Settings.builder().put("cluster.name","my-application").build()
val threadPool = new ThreadPool(settings)
val client = new NodeClient(settings, threadPool)
val searchDao = new org.nlpcn.es4sql.SearchDao(client)
val string = searchDao.explain(sql).explain().explain()
string
}
def getESdataBySearchAfterTemplate(){
val indexName = "filemetadata"
val query = new JHashMap[String, AnyRef]
val param = new JHashMap[String, AnyRef]
param.put("start", Long.box(1648540192929L))
query.put("attributes.name", "test0331")
query.put("lastAccessed",param)
val myMap: Map[String, Object] = Map("key1" -> "lastAccessed")
val sortField = myMap.get("key1").asInstanceOf[Option[String]]
val isAscending = false
val a = new ArrayList[AnyRef]
a.add("1648601617449")
a.add("e6590332010c44aca252e130fe418250")
//val str: Array[Object] = Array("aa", "bb")
println(a)
val size = 1
val response = ESUtils.getESdataBySearchAfterTemplate(
indexName,client,
query,
sortField,
isAscending,
a,
size
)
val getHits = response.getResponse.getHits
val hit = getHits.getHits
println(hit.length)
hit.map(
x => {
val MetaEntrystring = x.getSourceAsString
println(MetaEntrystring)
})
}
def main(args: Array[String]): Unit = {
//println("hehe")
//delete()
//addStorage()
//searchTemplate
//search01()
//val a = get("select * from filemetadata where lastAccessed = 1648540192929")
//测试sql转为es查询语句
/*val a = ESUtils.sqlToEsQuery("select * from filemetadata where lastAccessed = 1648540192929")
println(a)
import StructuredReaders._
val c : JMap[String, AnyRef] = JacksonStructuredParserFactory.Json.readFromText[JMap[String, AnyRef]](a)
val d= c.get("query").asInstanceOf[JMap[String, AnyRef]]
val result = new JHashMap[String, AnyRef]
result.put("customized", d)
println(result)*/
//get2(a)
getESdataBySearchAfterTemplate()
}
}
六 模板
{
"track_total_hits": true,
"query" : {
"bool" : {
"filter" : [
{{!文件名称}}
{{#name}}
{
"bool" : {
"should" : [{
"match" : {
"attributes.name" : "{{name}}"
}
}, {
"term" : {
"attributes.name.raw" : "{{name}}"
}
}],
"minimum_should_match" : 1
}
},
{{/name}}
{{!最后访问时间}}
{{#lastAccessed}}
{
"range": {
"lastAccessed" : {
{{#start}}
"gte" : "{{start}}",
{{/start}}
{{#end}}
"lte" : "{{end}}",
{{/end}}
"boost" : 1.0
}
}
},
{{/lastAccessed}}
{{!自定义查询}}
{{#customized}}
{{#toJson}}customized{{/toJson}}
{{/customized}}{{^customized}}
{
"match_all" : { }
}
{{/customized}}
]
}
},
"sort": [
{{#desc}}
{ "{{desc}}" : {"order" : "desc"}},
{{/desc}}
{{#descnullfirst}}
{ "{{descnullfirst}}" : {"order" : "desc", "missing" : "_first"}},
{{/descnullfirst}}
{{#asc}}
{ "{{asc}}" : {"order" : "asc"}},
{{/asc}}
{ "_id" : {"order" : "asc"}}
],
{{#from}}
"from" : {{from}},
{{/from}}{{^from}}
"search_after" : {{#toJson}}search_after{{/toJson}},
{{/from}}
"size" : {{size}}
}
https://www.icode9.com/content-1-1305391.html