How can I run a MarkLogic RestAPI call in the context of the query console?

Viewed 100

I would like to profile a RestAPI call to a custom function. It seems that I have a couple of options. Please comment on each:

  1. Extract the RestAPI call from the access log or request log for a given application server port and copy it to the query console. I don't know if this is possible, unless I invoke the RestAPI call with the xdmp:http-post or xdmp.httpPost API calls. I'm just not sure that profile a Rest call makes sense.
  2. I have a custom "zsearch" module/library that handles a search request and compresses the result. I think I can add the return:plan and return:metrics option to the parameters for the search query in the zsearch:get() function, but I don't understand the syntax for doing so in the code sample below where I want to hard-code the return calls. Can someone provide an example?

In addition, this module makes use of the search-model-query Rest API call at /MarkLogic/rest-api/models/search-model-query.xqy. I'm wondering where this module is located because I'd like to review it and add some logging to it. I'm not sure why the code uses the search-model-query.xqy library instead of the search:search call. It seems that the search:search invocation would be more efficient than what appears to be a subsequent Rest API call from within a database module.

I would also like to understand what would be required to port this code from using search-model-query.xqy to the search:search functionality described in https://docs.marklogic.com/search:search.

Here is the zsearch code:


module namespace zsearch = "http://marklogic.com/rest-api/resource/zsearch";

import module namespace lid = "http://marklogic.com/util/log-id"
  at "/MarkLogic/appservices/utils/log-id.xqy";

import module namespace searchmodq = "http://marklogic.com/rest-api/models/search-model-query"
  at "/MarkLogic/rest-api/models/search-model-query.xqy";

import module namespace sut = "http://marklogic.com/rest-api/lib/search-util"
  at "/MarkLogic/rest-api/lib/search-util.xqy";

import module namespace eput = "http://marklogic.com/rest-api/lib/endpoint-util"
  at "/MarkLogic/rest-api/lib/endpoint-util.xqy";

declare default function namespace "http://www.w3.org/2005/xpath-functions";

declare option xdmp:mapping "false";
(:
declare option xdmp:transaction-mode "auto";
:)

declare private function zsearch:response-callback(
  $response-type as xs:string?
) as empty-sequence()
{
 eput:response-type-callback($response-type),

 let $timestamp := xdmp:request-timestamp()
 return if (exists($timestamp)) then eput:add-response-header("ML-Effective-Timestamp",string($timestamp)) else ()
 
};

declare private function zsearch:query-parameter(
  $paramMap   as map:map,
  $params     as map:map,
  $name       as xs:string,
  $required   as xs:boolean,
  $repeatable as xs:boolean,
  $allowed    as xs:string*,
  $privileges as xs:string*,
  $converter  as function(*)?
) as map:map
{
  let $values := map:get($params,$name)
  return (
      if (not($required) or exists($values[. ne ""])) then ()
      else error((),"REST-REQUIREDPARAM",$name),

      if (empty($values))
      then $paramMap
      else (
          if (empty($privileges) or xdmp:has-privilege($privileges, "execute")) then ()
          else error((),"REST-INVALIDPARAM", concat(
              $name," parameter requires at least one privilege: ",string-join($privileges,", ")
              )),

          if ($repeatable or count($values) lt 2) then ()
          else error((),"REST-REPEATEDPARAM",$name),

          map:with($paramMap,$name,
              if (empty($allowed) and empty($converter))
              then $values
              else
                  let $fname  :=
                      if (empty($converter)) then ()
                      else function-name($converter)
                  let $fns    :=
                      if (empty($fname)) then ()
                      else namespace-uri-from-QName($fname)
                  let $flocal :=
                      if (not($fns = ("http://www.w3.org/2001/XMLSchema"))) then ()
                      else local-name-from-QName($fname)
                  for $val in $values
                  let $converted :=
                      if (empty($converter))
                      then $val
                      else if (empty($flocal) or xdmp:castable-as($fns,$flocal,$val))
                      then $converter($val)
                      else error((),"REST-INVALIDPARAM", concat(
                          $name, " parameter not convertible to xs:",
                          local-name-from-QName($fname)," value: ",string($val)
                          ))
                  return
                      if (exists($allowed) and not($converted = $allowed))
                      then error((),"REST-INVALIDTYPE",
                          ($name, $val, "is not one of", string-join($allowed, "|")))
                      else $converted
              )
          )
      )
};

declare private function zsearch:validate-parameter-names(
  $paramMap        as map:map,
  $params          as map:map,
  $ignore-patterns as xs:string*
) as xs:string*
{
  for $name in map:keys($params)
  return
      if (map:contains($paramMap,$name)) then ()
      else zsearch:check-ignore($name,$ignore-patterns)
};

declare private function zsearch:check-ignore(
  $name            as xs:string,
  $ignore-patterns as xs:string*
) as xs:string?
{
  if (exists($ignore-patterns) and exists(
      for $pattern in $ignore-patterns
      return
          if (matches($name,$pattern))
          then true()
          else ()
      ))
  then ()
  else $name
};

declare private function zsearch:compress(
  $data            as document-node()*
) as document-node()
{
  let $zip := xdmp:zip-create(
      <parts xmlns="xdmp:zip"><part>result.json</part></parts>,
      ($data)
  )
  return document { $zip }
};

declare function zsearch:get($context as map:map, $params as map:map) as document-node()* {

  let $headers   := eput:get-request-headers()
  let $accept    := eput:get-accept-types($headers)
  let $method    := eput:get-request-method($headers)
  let $env       := eput:response-callback-map(zsearch:response-callback#1)
  let $paramMap  := map:new()
      =>zsearch:query-parameter($params,"q",false(),false(),(),(),())
      =>zsearch:query-parameter($params,"category",false(),true(),("content","metadata","collections","permissions","properties","quality","metadata-values"),(),())
      =>zsearch:query-parameter($params,"format",false(),false(),("json","xml"),(),())
      =>zsearch:query-parameter($params,"start",false(),false(),(),(),xs:unsignedLong#1)
      =>zsearch:query-parameter($params,"pageLength",false(),false(),(),(),xs:unsignedLong#1)
      =>zsearch:query-parameter($params,"options",false(),false(),(),(),())
      =>zsearch:query-parameter($params,"collection",false(),true(),(),(),())
      =>zsearch:query-parameter($params,"directory",false(),false(),(),(),())
      =>zsearch:query-parameter($params,"view",false(),true(),("none","results","metadata","facets","all"),(),())
      =>zsearch:query-parameter($params,"txid",false(),false(),(),(),())
      =>zsearch:query-parameter($params,"database",false(),false(),(),(),())
      =>zsearch:query-parameter($params,"forest-name",false(),true(),(),(),())
      =>zsearch:query-parameter($params,"transform",false(),false(),(),(),())
      =>zsearch:query-parameter($params,"timestamp",false(),false(),(),(),())
      =>zsearch:query-parameter($params,"trace",false(),true(),(),"http://marklogic.com/xdmp/privileges/rest-tracer",())
  let $extra-names := zsearch:validate-parameter-names(
      if ($method = ("GET", "HEAD"))
          then zsearch:query-parameter($paramMap,$params,"structuredQuery",false(),false(),(),(),())
          else $paramMap,
      $params,
      ('name')
      )
  return (
      if (empty($extra-names)) then ()
      else error((),"REST-UNSUPPORTEDPARAM", concat(
          "invalid parameters: ",string-join($extra-names,", ")
          )),

      lid:enable(map:get($paramMap,"trace")),

      if (searchmodq:check-untraced()) then ()
      else lid:log(
          $searchmodq:trace-id,"zearch",
          map:entry("method",$method)=> map:with("headers",$headers)=> map:with("parameters",$paramMap)
          ),

      if (empty($accept)) then ()
      else if ($accept = ("application/json", "text/json", "application/xml", "text/xml")) then
          if (map:contains($paramMap,"category"))
          then error((),"REST-UNSUPPORTEDPARAM",
              "Can use the 'category' parameter only with multipart/mixed accept")
          else
              let $view := map:get($paramMap,"view")
              return
                  if (not($view eq "none")) then ()
                  else error((),"REST-UNSUPPORTEDPARAM",
                      "Can use the 'none' value for the 'view' parameter only with multipart/mixed accept")
      else if (starts-with(head($accept),"multipart/mixed"))
      then map:put($env,"add-header",eput:add-response-header#2)
      else error((), "REST-UNACCEPTABLETYPE", string-join($accept,", ")),

      let $_ := 1
      return (
          let $response :=  searchmodq:search-get($headers,$paramMap,$env)
          let $has-matches := map:get($env, "has-matches")
          return
              if (exists($response)) then 
(:
                  let $_ := map:put($context,"output-type","application/octet-stream") 
:)
                  let $_ := xdmp:set-response-content-type("application/octet-stream")
                  return zsearch:compress($response)
              else if ($has-matches) then ()
              else xdmp:set-response-code(404,"Not Found")
      )
  )
};```

1 Answers

You could use xdmp:http-get() or xdmp:http-post() to hit the actual endpoint and invoke it via HTTP with the appropriate action. And you can specify the return-plan and return-metrics in the options:

xdmp:http-get("http://localhost:8000/LATEST/config/resources/zsearch",
  <options xmlns="xdmp:http">
    <authentication method="basic">
      <username>myname</username>
      <password>mypassword</password>
      <return-metrics>true</return-metrics>
      <return-plan>true</return-plan>
    </authentication>
  </options>)

You could also import the installed REST module and invoke it's methods programmatically instead of through the HTTP endpoint:

   import module namespace zsearch = "http://marklogic.com/rest-api/resource/zsearch" 
     at "/marklogic.rest.resource/zsearch/assets/resource.xqy";
   let $context as map:map := map:new()
   let $params as map:map := map:new()
   return
     zsearch:get($context, $params)

You could also use xdmp:plan() and xdmp:query-meters()

The module /MarkLogic/rest-api/models/search-model-query.xqy can be found in the MarkLogic install for any MarkLogic server. It lives under the installed location at /Modules/MarkLogic/rest-api/models/search-model-query.xqy.

Related