Path Namespaces - Query Configuration
Namespace Handling in Path Expressions
Namespaces are one of the quietest ways to get completely wrong answers in MarkLogic — or more precisely, to get no answers at all. A query that returns an empty sequence when you know the data is there is disorienting. The database is not broken. The document is not missing. The only thing wrong is the expanded name, and that one character-level mismatch produces no error and no match.
It is an impressively subtle failure mode: technically correct, operationally annoying.
This article explains why namespace handling works the way it does, how to write queries and configure path range indexes correctly, and how to diagnose mismatches systematically when they occur.
XML Namespace Fundamentals
An XML namespace is a URI. It is not the prefix you see in the document, and it is not the element's local name. The real identity of an element is its expanded name: the combination of its local name and its namespace URI. Two elements with the same local name but different URIs are different elements. Two elements with different prefixes but the same URI are the same element.
There are three distinct namespace states an XML element can be in. The table below summarises them.
| State | What it looks like | What it means |
|---|---|---|
| Null namespace | <name>Marisol</name> with no xmlns in scope | The element has no namespace URI — it is in the null namespace |
| Default namespace | <llama-profile xmlns="http://example.com/llamas"> | All unprefixed child elements inherit the namespace URI silently |
| Prefix-bound namespace | <ll:llama-profile xmlns:ll="http://example.com/llamas"> | The element and its children are explicitly namespaced using a prefix |
The most common source of confusion is the default namespace case. When a document uses xmlns="http://example.com/llamas", every unprefixed element in scope — including <name>, <id>, and <height-cm> — is in the http://example.com/llamas namespace. The document shows no prefixes, but the elements are fully namespaced. A query that does not account for this will silently return nothing.
To illustrate, here is the XML document used throughout this article:
<llama-profile xmlns="http://example.com/llamas">
<id>llama-001</id>
<name>Marisol</name>
<height-cm>118</height-cm>
<habitat>
<region>Connemara</region>
</habitat>
</llama-profile>
The Silent Failure Pattern
Given that document, the most natural-looking XPath fails completely:
xquery version "1.0-ml";
(: No namespace declaration in this query. :)
(: The document uses a default namespace, so all elements are namespaced. :)
(: The path /llama-profile/name targets null-namespace elements — no match. :)
let $doc := doc('/cleverllamas/llamaverse/extensions/path-namespaces/llama-profile.xml')
return $doc/llama-profile/name
()
The path /llama-profile/name targets elements in the null namespace. Because the document uses a default namespace, none of its elements are in the null namespace. MarkLogic returns an empty sequence and reports no error. This is not a bug — it is precise and correct behaviour. It is just not what most people expect the first time they encounter it.
Declaring Namespaces in XQuery
XQuery gives you two ways to declare namespace intent in a module prolog.
Explicit prefix declaration
declare namespace ll = "http://example.com/llamas"; binds the prefix ll to the namespace URI for the duration of the module. You then use ll: on every element step in your path expressions.
xquery version "1.0-ml";
declare namespace ll = "http://example.com/llamas";
(: Bind the prefix ll to the namespace URI used by the document. :)
(: Now the path /ll:llama-profile/ll:name resolves to the correct element. :)
let $doc := doc('/cleverllamas/llamaverse/extensions/path-namespaces/llama-profile.xml')
return $doc/ll:llama-profile/ll:name
<name xmlns="http://example.com/llamas">Marisol</name>
The prefix itself is arbitrary — ll, ns, lp, or anything else will work provided it resolves to the same URI as the document. The URI is the identity; the prefix is a label.
Default element namespace declaration
declare default element namespace "http://example.com/llamas"; applies the given URI to every unprefixed element step in the module. You can then write /llama-profile/name without any prefix.
xquery version "1.0-ml";
declare default element namespace "http://example.com/llamas";
(: declare default element namespace applies the given URI to every :)
(: unprefixed element step in this module. The path /llama-profile/name :)
(: now resolves to the namespaced elements — no prefix is needed. :)
let $doc := doc('/cleverllamas/llamaverse/extensions/path-namespaces/llama-profile.xml')
return $doc/llama-profile/name
<name xmlns="http://example.com/llamas">Marisol</name>
This style is convenient for modules that query exclusively namespaced XML, but it creates a trap: if the module also accesses null-namespace elements, those paths will unexpectedly fail because the default namespace is now applied to them too. For shared application code and index definitions, explicit prefixes are generally safer.
The table below summarises both approaches.
| Approach | Prolog declaration | Path style | Best suited to |
|---|---|---|---|
| Explicit prefix | declare namespace ll = "..." | /ll:root/ll:child | Shared modules, index queries |
| Default element namespace | declare default element namespace "..." | /root/child | Single-namespace modules |
Declaring Namespaces in Server-Side JavaScript
Server-Side JavaScript does not support declare namespace as a module-level statement. Instead, namespace declarations go directly inside the XPath string, at the start of the expression. This keeps the namespace binding local to the path evaluation and makes the path self-contained.
Inspecting the namespace of a stored node
Before writing any XPath, use fn.namespaceURI() and fn.localName() to confirm what namespace a stored element actually carries:
'use strict';
// fn.namespaceURI() and fn.localName() inspect the expanded name of a node.
// Use these to prove what namespace a stored XML element actually carries.
const doc = fn.doc('/cleverllamas/llamaverse/extensions/path-namespaces/llama-profile.xml');
const parts = Array.from(xdmp.xqueryEval(
'xquery version "1.0-ml"; declare variable $d external; (fn:local-name($d/*), fn:namespace-uri($d/*))',
{ d: doc },
{ update: 'false' }
));
({
localName: fn.string(parts[0]),
namespaceURI: fn.string(parts[1])
});
{
"localName": "llama-profile",
"namespaceURI": "http://example.com/llamas"
}
A non-empty namespaceURI tells you immediately that you must account for a namespace in any XPath against this document.
XPath with an inline namespace declaration
'use strict';
// In Server-Side JavaScript, namespace declarations go inside the XPath string.
// The declare namespace clause must appear at the start of the expression.
const doc = fn.doc('/cleverllamas/llamaverse/extensions/path-namespaces/llama-profile.xml');
const query = 'xquery version "1.0-ml"; declare namespace ll="http://example.com/llamas"; declare variable $d external; string($d/ll:llama-profile/ll:name)';
fn.string(fn.head(xdmp.xqueryEval(
query,
{ d: doc },
{ update: 'false' }
)));
Marisol
The declare namespace ll="..."; clause at the start of the path string is the JavaScript equivalent of declare namespace in XQuery. The namespace must be inside the string because doc.xpath() evaluates the path in its own expression context.
Path Range Indexes and Namespaces
Path range indexes do not have their own namespace system. They use path expressions — and path expressions use expanded names. That means every rule that applies to XPath queries applies equally to path range index definitions.
A path range index configured with the path /llama-profile/name targets null-namespace elements. If the documents use a default namespace, the index builds successfully, stores no data, and returns nothing when queried. This is the same silent failure pattern, but it can cost hours to diagnose because the Admin UI gives no indication that the configuration is semantically wrong.
What the Admin UI Fields Really Mean
When you configure a path range index in the Admin UI, you provide two logically related things:
- A path expression, such as
/ll:llama-profile/ll:height-cm - Namespace bindings that assign a URI to each prefix used in that path
The prefix in the path expression is just a label. The namespace binding field gives that label meaning. If the URI is wrong, the index will not match the intended documents. If the prefix used in the path is missing from the namespace bindings, the path expression is incomplete.
| Admin UI field | Example value | Why it matters |
|---|---|---|
| Path Expression | /ll:llama-profile/ll:height-cm | The path MarkLogic indexes — must use correct expanded names |
| Namespace Prefix | ll | The prefix used in the path expression |
| Namespace URI | http://example.com/llamas | Must match the document namespace exactly |
| Scalar Type | int | Determines how values are indexed and queried |
| Collation | e.g. http://marklogic.com/collation/ | Required for string types; controls comparison behaviour |
A working index configuration for the llama-profile.xml document above looks like this:
- Path expression:
/ll:llama-profile/ll:height-cm - Namespace prefix:
ll - Namespace URI:
http://example.com/llamas - Scalar type:
int
Querying a Path Range Index From XQuery
When querying a path range index, the path and namespace bindings in the query must agree with those in the index definition. The cts:path-range-query() function accepts a namespace bindings map as its seventh argument:
xquery version "1.0-ml";
(: The path expression and the namespace binding map must agree. :)
(: The path uses ll: as a prefix; the map binds ll to the same URI :)
(: that the document uses. Any prefix works, as long as the URI matches. :)
declare namespace ll = "http://example.com/llamas";
let $query := cts:path-range-query(
"/ll:llama-profile/ll:height-cm",
">=",
115
)
for $doc in cts:search(fn:collection(), $query)
return xdmp:node-uri($doc)
/cleverllamas/llamaverse/extensions/path-namespaces/marisol.xml
/cleverllamas/llamaverse/extensions/path-namespaces/solana.xml
The URI in the map must match the URI used when the index was created. The prefix can differ between the index definition and the query — again, only the URI matters for matching.
Descendant Paths and Namespaces
A common misconception is that //element relaxes namespace rules because it broadens the structural search. It does not. //element is short for /descendant-or-self::node()/child::element(element), and the name test still resolves by expanded name. //region still targets null-namespace region elements; //ll:region targets region elements in the http://example.com/llamas namespace.
xquery version "1.0-ml";
declare namespace ll = "http://example.com/llamas";
(: Comparing anchored vs descendant path expressions. :)
(: Neither broadens namespace rules — both require the correct expanded name. :)
let $doc :=
<llama-profile xmlns="http://example.com/llamas">
<habitat>
<region>Connemara</region>
</habitat>
</llama-profile>
return (
$doc//region, (: targets null namespace — no match :)
$doc//ll:region, (: targets http://example.com/llamas — matches :)
$doc/ll:habitat/ll:region (: wrong path: habitat is not the document root :)
)
()
<region xmlns="http://example.com/llamas">Connemara</region>
()
Being structurally broader than an anchored path does not make a descendant path namespace-broader. Both require the correct expanded name at every step.
Diagnosing Namespace Mismatches Systematically
When a path query returns nothing and you are confident the data is there, work through this checklist before touching the query or the index:
- Inspect the raw stored document — use
fn:doc()or Query Console against the actual URI, not a transformed copy from application code. - Run
fn:namespace-uri()andfn:local-name()on the target element — prove what namespace and local name the stored node actually carries. - Compare the namespace URI character by character against the Admin UI binding and the query map —
httpvshttps, trailing slashes, and case differences are all invisible to the naked eye but cause total mismatches. - Test a plain XPath against the document before testing a
cts:path-range-query()— if the XPath fails, the indexed query will fail for the same reason. - Check collation and scalar type only after the namespace is confirmed.
The diagnostic script below automates the first few checks:
xquery version "1.0-ml";
declare namespace ll = "http://example.com/llamas";
(: Diagnostic: use this script to prove what namespace a stored document :)
(: actually carries before debugging a path range index query. :)
let $doc := fn:doc('/cleverllamas/llamaverse/extensions/path-namespaces/llama-profile.xml')
return map:new((
map:entry('root-local-name', fn:local-name($doc/*)),
map:entry('root-namespace', fn:namespace-uri($doc/*)),
map:entry('name-exists-unprefixed', fn:exists($doc/llama-profile/name)),
map:entry('name-exists-prefixed', fn:exists($doc/ll:llama-profile/ll:name)),
map:entry('name-value', fn:string($doc/ll:llama-profile/ll:name))
))
{
"root-local-name": "llama-profile",
"root-namespace": "http://example.com/llamas",
"name-exists-unprefixed": false,
"name-exists-prefixed": true,
"name-value": "Marisol"
}
The name-exists-unprefixed: false and name-exists-prefixed: true output makes the mismatch explicit at a glance. Run this whenever a stored document is not behaving as expected.
The four most common failure modes worth memorising are:
| Failure mode | Symptom | Fix |
|---|---|---|
| Default namespace, path omits prefix | Empty results | Declare a prefix or default element namespace in the query |
| Path range index uses unprefixed path for namespaced elements | Index builds; never matches | Reconfigure index with namespace-qualified path |
| Query prefix bound to wrong URI | Empty results | Correct the URI in the namespace map |
URI differs subtly (http vs https, trailing slash) | Empty results | Copy URI directly from a verified source |
JSON Documents and the Namespace-Free Lane
JSON property names are not XML element names. They carry no namespace URI and no inheritance from parent nodes. When you query JSON with cts:json-property-value-query() or cts:json-property-range-query(), you pass the property name as a plain string — no namespace prefix is involved.
The llamaverse stores llama data as JSON documents in the wild-llamas collection. A query for llamas named Aaron requires no namespace handling at all:
xquery version "1.0-ml";
(: JSON property queries do not use XML namespace declarations. :)
(: Property names are strings, not XML expanded names. :)
(: This query finds all wild llamas named Aaron — no prefix needed. :)
for $doc in cts:search(
fn:collection("wild-llamas"),
cts:json-property-value-query("name", "Aaron")
)[1 to 3]
return xdmp:node-uri($doc)
/cleverllamas/llamaverse/raw/wild-llamas/llamas/8ee6e559-6460-489e-8f30-9366f9bbf41c.json
/cleverllamas/llamaverse/raw/wild-llamas/llamas/7b1de50d-4d0d-43eb-9cfb-2643dc4b9bc5.json
/cleverllamas/llamaverse/raw/wild-llamas/llamas/d4fa7481-4044-41f6-979b-cfe106aae806.json
The simplicity here is real, not incidental. It reflects the fact that JSON has a different data model. The absence of namespace declarations is not a shortcut — it is accurate.
The examples in this article assume the llamaverse (v2.3.1+) is deployed. The llamaverse sample data is freely available from github.com/cleverllamas/llamaverse — see the llamaverse article for full setup instructions.
Mixed XML and JSON Databases
Many production MarkLogic databases contain both XML and JSON. The important discipline is to keep the mental models separate: XML namespace rules apply to XML; JSON property rules apply to JSON. Carrying XML namespace declarations into a JSON property query accomplishes nothing. Assuming JSON property names behave like XML local names leads to confusion when the same prefix handling that works for XML has no effect on JSON.
| Data format | Typical index type | Name resolution rule | Common pitfall |
|---|---|---|---|
| XML | Path range index | Expanded name including namespace URI | Default namespace treated as if no namespace |
| JSON | JSON property or property range index | Property name exactly as stored | Assuming XML namespace declarations apply |
| Mixed database | Both, separately | Each query type follows its own model | Reusing XML path habits in JSON code |
Quick Reference
XQuery vs JavaScript namespace syntax
| Task | XQuery | Server-Side JavaScript |
|---|---|---|
| Declare a prefix | declare namespace ll = "http://example.com/llamas"; | Include declare namespace ll="..."; inside the XPath string |
| Default element namespace | declare default element namespace "..."; | Not available as a module declaration; use inline prefix declarations in XPath strings |
| Inspect element namespace | fn:namespace-uri($node) | fn.namespaceURI(node) |
| Inspect local name | fn:local-name($node) | fn.localName(node) |
| Debug a node | xdmp:describe($node) | xdmp.describe(node) |
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Querying a default-namespaced document without a namespace declaration | Empty results | Declare a prefix and use /ns:root/ns:child, or declare a default element namespace |
| Configuring a path range index with an unprefixed path for namespaced XML | Index builds but never matches | Reconfigure with the correct namespace-qualified path |
| Binding the right prefix to the wrong URI | Empty results | Compare the URI character by character with namespace-uri() output |
| Assuming the document prefix must match the query prefix | Unnecessary rewrites | Any prefix works; only the URI must match |
| Using XML namespace declarations in JSON property queries | Queries do not behave as intended | Use JSON property names directly — no prefix needed |
Assuming //element ignores namespaces | Empty results from descendant queries | Declare and use the correct namespace for descendant steps too |
FAQ
Does the query prefix have to match the document prefix?
No. Only the namespace URI must match. You can use any prefix you like in a query, as long as you bind it to the same URI the document uses.
If the XML shows no prefixes, is it unnamespaced?
Not necessarily. A default namespace declaration silently namespaces every descendant element even when no prefixes are visible. Run fn:namespace-uri() against a stored node to be certain.
Does //name ignore namespaces?
No. It broadens the structural search — descendants rather than direct children — but does not relax namespace rules. //name still targets null-namespace name elements.
Why did my path range index build successfully if the path is wrong?
Because the index definition is syntactically valid. MarkLogic validates that the path expression and namespace bindings are well-formed, not that they match any documents in the database.
What is the fastest first diagnostic step?
Run fn:namespace-uri() against the actual stored node. If it returns a non-empty string, you must account for that URI in every path expression targeting that element.
Do this first, and you usually save yourself an hour of creative but unhelpful troubleshooting.
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 or training!
- XML Namespace Fundamentals
- The Silent Failure Pattern
- Declaring Namespaces in XQuery
- Explicit prefix declaration
- Default element namespace declaration
- Declaring Namespaces in Server-Side JavaScript
- Inspecting the namespace of a stored node
- XPath with an inline namespace declaration
- Path Range Indexes and Namespaces
- What the Admin UI Fields Really Mean
- Descendant Paths and Namespaces
- Diagnosing Namespace Mismatches Systematically
- JSON Documents and the Namespace-Free Lane
- Mixed XML and JSON Databases
- Quick Reference
- XQuery vs JavaScript namespace syntax
- Common mistakes
- FAQ