One Indexing Model for JSON and XML

Match Query Functions to the Right Index Across Both Document Models

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

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 typeFunctionIndex requiredNotes
Exact value matchcts:json-property-value-queryNone (universal index)Case-insensitive, requires exact match of entire value
Word within valuects:json-property-word-queryNo range index; word-query behaviour depends on DB settingsFinds partial matches within a value; exact behaviour depends on configured word-query options
Comparison (>, <, >=, <=) by property namects:json-property-range-queryRange element indexRequired for any range query on a named property
Comparison (>, <, >=, <=) by path expressioncts:path-range-queryRange path indexRequired for range queries using exact path matching
Lexicon/faceting by property namects:element-values / cts:element-referenceRange element indexUnlocks efficient faceted search without reading documents
Lexicon/faceting by pathcts:values / cts:path-referenceRange path indexEfficient distinct-value retrieval via exact path
Sort by property namects:index-order(cts:element-reference(...))Range element indexLeverages index for sorted result sets
Sort by pathcts:index-order(cts:path-reference(...))Range path indexLeverages 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

  1. Do you need comparison operators (>, <, >=, <=)? → You need a range index.
  2. Do you need faceting/distinct values? → You need a range index (which also provides a lexicon).
  3. Do you need sorting by property? → You need a range index.
  4. 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-ELEMRIDXNOTFOUND or XDMP-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!