cts:document-root-query()

Released in MarkLogic 11

personClever Llamas
CleverLlamasMinimum Llamaverse Version: 2
databaseMinimum MarkLogic Version: 11

Before this feature, we could do a few things when querying about an element name:

  • cts:element-query(xs:XName("my-element"), cts:true-query()). However, this does not say if the element exists as root - just that it exists.
  • You could do a cts:search(doc()/my-element, cts:true-query(), "unfiltered") with the first parameter using an XPath to the root directory. However, this is inefficient and can fill the expanded tree cache. Furthermore, in server-side JavaScript, that direct approach is not practical.
  • You could create a TDE template with a context of / and then create a column or triple with the root node name. However, this only works for XML and not JSON.

To test the feature fully, we set up some sample documents including enough to run some negative tests.

API Context for This Article

Context ItemWhat this article uses
Data setpurpose-built XML and JSON samples inserted by the setup scripts in this article
Primary document scopecollection /clever-llamas/test/cts-document-root-query
Primary query featurects:document-root-query(xs:QName(...))
Fields or views usedNone
Index contextRoot-name matching resolved via cts query evaluation; behaviour differs between XML root elements and JSON top-level properties

If results are unexpected, first check whether the document is XML or JSON and whether the target name appears at the correct top level.

Initial Setup

  • XML with an expected root node
  • JSON - with a single top level property with expected same name as root node
  • XML with a different root node
  • JSON with a single top level property with a different name
<my-element>
  <title>Root level element</title>
  <description>Used to demonstrate document root matching.</description>
</my-element>
xquery version "1.0-ml";

let $permissions := xdmp:default-permissions((), "objects")
let $options := map:entry("permissions", $permissions) => map:with("collections", "/clever-llamas/test/cts-document-root-query")
return (
  xdmp:document-insert("/clever-llamas/test/cts-document-root-query/sample-llama.json", xdmp:unquote('{ llama : { species : "Suri Llama"}}'), $options),
  xdmp:document-insert("/clever-llamas/test/cts-document-root-query/sample-llama.xml", element llama { element species {"Suri Llama"}}, $options),
  xdmp:document-insert("/clever-llamas/test/cts-document-root-query/sample-not-llama.json", xdmp:unquote('{ bird : { species : "Emu"}}'), $options),
  xdmp:document-insert("/clever-llamas/test/cts-document-root-query/sample-not-llama.xml", element bird { element species {"Emu"}}, $options)
);

fn:collection("/clever-llamas/test/cts-document-root-query") ! function($doc) { (xdmp:node-uri($doc), $doc) }(.)
'use strict';

const collection = '/clever-llamas/test/cts-document-root-query';
const options = {
  permissions: xdmp.defaultPermissions(),
  collections: [collection]
};

xdmp.documentInsert(
  `${collection}/sample-llama.json`,
  xdmp.unquote('{"llama":{"species":"Suri Llama"}}'),
  options
);
xdmp.documentInsert(
  `${collection}/sample-llama.xml`,
  xdmp.unquote('<llama><species>Suri Llama</species></llama>'),
  options
);
xdmp.documentInsert(
  `${collection}/sample-not-llama.json`,
  xdmp.unquote('{"bird":{"species":"Emu"}}'),
  options
);
xdmp.documentInsert(
  `${collection}/sample-not-llama.xml`,
  xdmp.unquote('<bird><species>Emu</species></bird>'),
  options
);

const results = fn.collection(collection).toArray().map((doc) => ({
  uri: xdmp.nodeUri(doc),
  nodeKind: xdmp.nodeKind(doc)
}));

({
  sample: 'cts/document-root-query/assets/setup.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});

List of documents from setup script

Test

Simple test of the feature:

<my-element>
  <title>Root level element</title>
  <description>Used to demonstrate document root matching.</description>
</my-element>
xquery version "1.0-ml";

let $query := cts:and-query((
  cts:collection-query("/clever-llamas/test/cts-document-root-query"),
  cts:document-root-query(xs:QName("llama"))
))

return
for $uri in cts:uris((), (), $query)
return ($uri, fn:doc($uri))
'use strict';

const collection = '/clever-llamas/test/cts-document-root-query';
const query = cts.andQuery([
  cts.collectionQuery(collection),
  cts.documentRootQuery(cts.wordQuery('llama'))
]);

const results = cts.uris(null, ['limit=10'], query).toArray();

({
  sample: 'cts/document-root-query/assets/test-document-root-query.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});

Result:Result showing expected documents

At first glance, this appears to work perfectly. However, we still wanted to validate what "document root" means for JSON. Based on prior JSON/TDE root-context (/) behaviour, a deeper check was warranted.

Let's look at what MarkLogic returns when we access the root node of a JSON document:

<my-element>
  <title>Root level element</title>
  <description>Used to demonstrate document root matching.</description>
</my-element>
xquery version "1.0-ml";

doc("/clever-llamas/test/cts-document-root-query/sample-llama.json")/node() ! xdmp:node-kind(.)
'use strict';

const uri = '/clever-llamas/test/cts-document-root-query/sample-llama.json';
const doc = fn.doc(uri);

const results = {
  uri,
  nodeKind: xdmp.nodeKind(doc)
};

({
  sample: 'cts/document-root-query/assets/json-root-node-kind.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});

Result:Result showing node-kind = Object

There is no single "root" element in a JSON document that has a name. We tested some more:

Setup 2

We added a document with 2 different top-level properties

<my-element>
  <title>Root level element</title>
  <description>Used to demonstrate document root matching.</description>
</my-element>
xquery version "1.0-ml";

let $permissions := xdmp:default-permissions((), "objects")
let $options := map:entry("permissions", $permissions) => map:with("collections", "/clever-llamas/test/cts-document-root-query")
return
  xdmp:document-insert(
    "/clever-llamas/test/cts-document-root-query/sample-more-than-llamas.json",
    xdmp:unquote('{ llama : { species : "Suri Llama"}, snake : { species : "Amazon Tree Boa"}}'),
    $options
  );

fn:doc("/clever-llamas/test/cts-document-root-query/sample-more-than-llamas.json") ! function($doc) { (xdmp:node-uri($doc), $doc) }(.)
'use strict';

const uri = '/clever-llamas/test/cts-document-root-query/two-top-level-properties.json';
xdmp.documentInsert(
  uri,
  xdmp.unquote('{"llama":{"species":"Suri Llama"},"farm":{"name":"Clever Llamas"}}'),
  {
    permissions: xdmp.defaultPermissions(),
    collections: ['/clever-llamas/test/cts-document-root-query']
  }
);

const doc = fn.doc(uri);
const results = {
  uri: xdmp.nodeUri(doc),
  nodeKind: xdmp.nodeKind(doc)
};

({
  sample: 'cts/document-root-query/assets/setup-two-top-level-properties.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});

Document with 2 properties

When we run the same test again:

<my-element>
  <title>Root level element</title>
  <description>Used to demonstrate document root matching.</description>
</my-element>
xquery version "1.0-ml";

let $query := cts:and-query((
  cts:collection-query("/clever-llamas/test/cts-document-root-query"),
  cts:document-root-query(xs:QName("llama"))
))

return
for $uri in cts:uris((), (), $query)
return ($uri, fn:doc($uri))
'use strict';

const collection = '/clever-llamas/test/cts-document-root-query';
const query = cts.andQuery([
  cts.collectionQuery(collection),
  cts.documentRootQuery(cts.wordQuery('llama'))
]);

const results = cts.uris(null, ['limit=10'], query).toArray();

({
  sample: 'cts/document-root-query/assets/test-document-root-query.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});

Result:Document with 2 JSON properties also returned

This result shows that, for JSON documents, MarkLogic looks at the name of any top-level property when resolving cts:document-root-query().

We then tested further by nesting the same structures one level deeper, confirming that XML matching remains root-level and JSON matching remains top-level-property-based.

Conclusion

For XML, this behaves as expected: cts:document-root-query() targets the document root element.

For JSON, it matches any top-level property with the requested name. That sounds like a small detail. It is not. If your JSON model does not enforce a single envelope key, you can get broader matches than intended. Treat envelope discipline as a query-safety rule, not a style preference.

Need Some Help?


Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting!
  • API Context for This Article