One Indexing Model for JSON and XML
Match Query Functions to the Right Index Across Both Document Models
The examples in this article use the llamaverse (v2.0+). The llamaverse sample data is freely available from github.com/cleverllamas/llamaverse — see the llamaverse article for full setup instructions.
The Core Problem
Different JSON query functions require different indexes — or no index at all. Use the wrong combination and MarkLogic does not quietly fall back. It throws.
Example: You have a range path index on /heightCm but try to use cts:json-property-range-query("heightCm", ...). Result: XDMP-ELEMRIDXNOTFOUND (you need a range element index, not a path index).
This is intentional design. MarkLogic refuses to execute an unindexed range scan across millions of documents because it would be far too slow to be useful.
Quick Reference — Which Query, Which Index?
| Query type | Function | Index required | Notes |
|---|---|---|---|
| Exact value match | cts:json-property-value-query | None (universal index) | Case-insensitive, requires exact match of entire value |
| Word within value | cts:json-property-word-query | No range index; word-query behaviour depends on DB settings | Finds partial matches within a value; exact behaviour depends on configured word-query options |
| Comparison (>, <, >=, <=) by property name | cts:json-property-range-query | Range element index | Required for any range query on a named property |
| Comparison (>, <, >=, <=) by path expression | cts:path-range-query | Range path index | Required for range queries using exact path matching |
| Lexicon/faceting by property name | cts:element-values / cts:element-reference | Range element index | Unlocks efficient faceted search without reading documents |
| Lexicon/faceting by path | cts:values / cts:path-reference | Range path index | Efficient distinct-value retrieval via exact path |
| Sort by property name | cts:index-order(cts:element-reference(...)) | Range element index | Leverages index for sorted result sets |
| Sort by path | cts:index-order(cts:path-reference(...)) | Range path index | Leverages index for sorted result sets using path |
Key rule: If you don't need comparison, faceting, or sorting, you don't need a range index.
Queries That Need No Index
The simplest JSON property queries do not require a range index. They resolve through the universal index, but word-query behaviour still depends on database word-query settings and options.
Exact Value Match: cts:json-property-value-query
This function matches a complete property value (case-insensitive). A property with value "Suri llama" does not match a value query for "Suri" alone — the entire value must match exactly.
Use this for discrete, short-lived values: breed codes, status fields, type identifiers.
xquery version "1.0-ml";
(: cts:json-property-value-query matches documents where the named property :)
(: has exactly the given value (case-insensitive by default). :)
(: Uses the universal index — no dedicated range index required. :)
fn:count(
cts:search(
fn:collection("wild-llamas"),
cts:json-property-value-query("breed", "Suri")
)
)
1047
Word Match Within Value: cts:json-property-word-query
This function matches if the target word appears anywhere within the property value. A value of "Suri llama" matches a word query for "Suri" because "Suri" is a word within it.
Use this for longer text values where partial matches are expected.
These two query functions do not require range-index configuration. For cts:json-property-word-query, behaviour still depends on word-query settings and options. Move to range queries below only if you need comparison operators (>, <, >=, <=) or faceting.
Range Queries Require Specific Index Types
For comparison operators (>, <, >=, <=) you need a range index. But there are two independent range index types, and using the wrong query function for your index throws an error.
The Critical Error: Index-Query Mismatch
Here is what catches developers out: cts:json-property-range-query and cts:path-range-query use different index types. Configuring one does not satisfy the other.
The llamaverse heightCm property has a path range index configured at path /heightCm. If you try to use cts:json-property-range-query("heightCm", ...) instead, you get an immediate error:
xquery version "1.0-ml";
(: heightCm has a path range index (/heightCm) but NOT a range element index. :)
(: cts:json-property-range-query uses the element range index, not the path :)
(: range index. These two index types are independent. :)
(: This query will throw XDMP-ELEMRIDXNOTFOUND. :)
cts:search(
fn:collection("wild-llamas"),
cts:json-property-range-query("heightCm", ">", xs:int(190))
)
XDMP-ELEMRIDXNOTFOUND: cts:search(...) -- No int element range index for heightCm
XDMP-ELEMRIDXNOTFOUND: There is no element range index for heightCm. The query does not silently scan — it fails. This is intentional: an unindexed range scan across millions of documents would be far too slow.
The equivalent error for path queries: using cts:path-range-query("/breed", ...) fails with XDMP-PATHRIDXNOTFOUND if only an element range index exists (not a path index).
Lesson: Check the quick reference table above. Use the right query function for the index you configured.
Range Element Index + cts:json-property-range-query
A range element index (configured by property name) enables the cts:json-property-range-query function. MarkLogic treats JSON property names like XML element names for index purposes.
Configure via Admin UI: Databases → db → Range Element Indexes. Or use the Management REST API with key range-element-index.
The llamaverse breed property has a range element index configured with codepoint collation:
xquery version "1.0-ml";
(: cts:json-property-range-query enables comparison operators. :)
(: Requires a range element index on the property name. :)
(: The breed property has a range element index with codepoint collation. :)
(: "Suri" and "Hybrid" both sort after "Huacaya". :)
fn:count(
cts:search(
fn:collection("wild-llamas"),
cts:json-property-range-query("breed", ">", "Huacaya")
)
)
2013
Breeds in alphabetical order: Huacaya, Hybrid, Suri. A query for > "Huacaya" returns the 2,013 llamas whose breed sorts after "Huacaya" (Hybrid and Suri combined).
Range Path Index + cts:path-range-query
A range path index (configured by XPath/JSON path expression) enables the cts:path-range-query function. Path indexes match by exact path — the path expression in the query must match the configured path exactly, including the leading slash for top-level properties.
Configure via Admin UI: Databases → db → Range Path Indexes. Or use the Management REST API with key range-path-index.
The llamaverse heightCm property has a path range index at /heightCm with type int:
xquery version "1.0-ml";
(: cts:path-range-query uses a path expression that must match a configured :)
(: range path index exactly (including leading slash). :)
(: The /heightCm path range index is configured as type int. :)
fn:count(
cts:search(
fn:collection("wild-llamas"),
cts:path-range-query("/heightCm", ">", xs:int(190))
)
)
283
Use element indexes when you know the property name and it appears at arbitrary depths. Use path indexes when you need to match a specific path — useful when the same property name appears at different depths with different meanings.
Unlocking Faceting and Distinct Values via Lexicons
Range indexes do more than enable comparison queries. They also populate lexicons — in-memory structures that let MarkLogic return all distinct values for a property (with frequencies) without reading any documents.
This is the foundation of efficient faceted search. The breed range element index makes a lexicon available via cts:element-values:
xquery version "1.0-ml";
(: cts:element-values reads the breed lexicon — all indexed values with :)
(: document frequencies. This only works because there is a range element :)
(: index on "breed". Without it, this call throws XDMP-LEXNOTFOUND. :)
for $v in cts:element-values(xs:QName("breed"), (), ("item-frequency"))
return $v || " (" || cts:frequency($v) || ")"
Huacaya (1027)
Hybrid (966)
Suri (1047)
This reads only the index — no llama documents are opened. For 3,000 documents that's negligible either way; for 100 million documents, the difference between a lexicon scan and a document scan is enormous.
Path range indexes also expose lexicons via cts:values with a cts:path-reference.
Faceting Requirement
If you need faceting, sorting, or typeahead on a property, you need a range index. Value and word queries alone do not populate a lexicon.
Configuration
Range Element Index (for cts:json-property-range-query)
Configure via the Management REST API using the range-element-index key:
{
"range-element-index": [
{
"scalar-type": "string",
"namespace-uri": "",
"localname": "breed",
"collation": "http://marklogic.com/collation/codepoint",
"range-value-positions": false,
"invalid-values": "reject"
}
]
}
Or via the Admin UI: Databases → db → Range Element Indexes → Add.
Range Path Index (for cts:path-range-query)
Configure via the Management REST API using the range-path-index key:
{
"range-path-index": [
{
"scalar-type": "int",
"path-expression": "/heightCm",
"collation": "",
"range-value-positions": false,
"invalid-values": "reject"
}
]
}
Or via the Admin UI: Databases → db → Range Path Indexes → Add.
Both index types trigger a reindex when added to a populated database.
Pro Tip 💪
Adding a range index is only useful if you then refactor your code to use it.
Summary and Decision Flow
- Do you need comparison operators (>, <, >=, <=)? → You need a range index.
- Do you need faceting/distinct values? → You need a range index (which also provides a lexicon).
- Do you need sorting by property? → You need a range index.
- Do you only need exact or word-match queries? → You usually do not need a range index. Word-query behaviour still depends on DB word-query settings and options.
If you do need a range index:
- Property name matters, arbitrary depth? → Use a range element index with
cts:json-property-range-query - Exact path matters? → Use a range path index with
cts:path-range-query
Refer to the quick reference table at the top of this article for the complete decision matrix.
Final Takeaways
- Word and value queries typically do not need a range index, but word-query behaviour depends on database word-query settings and options.
- Range queries require a specifically configured range index. There are two types: element (by property name) and path (by path expression). They are independent.
- Using the wrong query function for your index type throws
XDMP-ELEMRIDXNOTFOUNDorXDMP-PATHRIDXNOTFOUND. MarkLogic refuses silent fallback because unindexed range scans are too slow. - Range indexes unlock lexicon access, which is essential for efficient faceting, sorting, and distinct-value retrieval without reading documents.
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!
- The Core Problem
- Quick Reference — Which Query, Which Index?
- Queries That Need No Index
- Exact Value Match: cts:json-property-value-query
- Word Match Within Value: cts:json-property-word-query
- Range Queries Require Specific Index Types
- The Critical Error: Index-Query Mismatch
- Range Element Index + cts:json-property-range-query
- Range Path Index + cts:path-range-query
- Unlocking Faceting and Distinct Values via Lexicons
- Configuration
- Range Element Index (for cts:json-property-range-query)
- Range Path Index (for cts:path-range-query)
- Summary and Decision Flow
- Final Takeaways