URI Directory Queries in MarkLogic

How Slash Hierarchies Shape Query Scope

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

Directories in MarkLogic are not folders. There is no physical directory on disk — a directory is a logical grouping derived from URI structure. Any URI of the form /foo/bar/document.json implies a directory at /foo/ and a directory at /foo/bar/. Those directories come into existence automatically when the document is inserted, and cts:directory-query() uses them to scope searches and listings.

Three things catch developers off guard: directories are created automatically so you never need to create them in advance; the depth parameter on cts:directory-query() controls exactly which level of the hierarchy is queried, and choosing the wrong depth returns zero results with no error; and explicit directory creation serves a specific purpose — setting path defaults on a URI prefix before ingestion — which is different from creating the directory hierarchy itself.

Sample Data

The examples in this article assume the llamaverse (v2.0+) is deployed. The llamaverse uses a well-defined URI hierarchy with multiple layers: raw llama profiles, content subdirectories for pets, schools, animal shelters, and more. It provides ideal real-world data for demonstrating directory behaviour at scale.

The llamaverse sample data is freely available from github.com/cleverllamas/llamaverse — see the llamaverse article for full setup instructions.

URI Design and the Directory Model

Why URI Structure Matters

Because directories derive from URI structure, your URI naming conventions directly determine the shape of your directory hierarchy and what directory queries are possible. A flat URI scheme — all documents at one level — makes directory-scoped queries useless. A deep, well-organised scheme lets you scope operations precisely to any subtree.

The Llamaverse Convention

The llamaverse uses a consistent three-to-five level hierarchy:

/cleverllamas/llamaverse/
  raw/
    wild-llamas/
      llamas/             ← 3,000 llama profile JSON files
  content/
    pets/                 ← 965 pet JSON files
    schools/              ← 6 school JSON files
    animal-shelters/      ← 15 shelter JSON files
    professions/          ← 10 profession JSON files
    secret-powers/        ← 20 secret power JSON files

This structure means you can:

  • Target all 3,000 llama profiles with cts:directory-query("/cleverllamas/llamaverse/raw/wild-llamas/llamas/", "1")
  • Target all content documents with cts:directory-query("/cleverllamas/llamaverse/content/", "infinity")
  • Target a specific content type precisely with cts:directory-query("/cleverllamas/llamaverse/content/schools/", "1")
  • Target everything in the llamaverse with cts:directory-query("/cleverllamas/llamaverse/", "infinity")

Each of those query shapes works because the URI convention was designed with them in mind. Design your URI structure before you begin ingestion — it is expensive to reorganise later.

URI-Derived Directory Structure

How it Works

Every time you insert a document, MarkLogic registers a directory entry for each path component in the URI. Inserting a llama profile at /cleverllamas/llamaverse/raw/wild-llamas/llamas/00048384-cb13-4557-805e-a6b4e57f7eab.json causes MarkLogic to register directories at /cleverllamas/, /cleverllamas/llamaverse/, /cleverllamas/llamaverse/raw/, /cleverllamas/llamaverse/raw/wild-llamas/, and /cleverllamas/llamaverse/raw/wild-llamas/llamas/ — all in a single step, without any explicit directory creation call.

In the normal case, those logical directories are not documents. They have no content, no collections, and no properties unless a directory document has been created explicitly. They are index entries that allow cts:directory-query() to resolve which documents belong to which path prefix.

The Directory-Creation Setting

Do not confuse the database directory-creation setting with directory-query behaviour. The documented xdmp:database-directory-creation() function reports whether the database is set to create directory documents automatically, but cts:directory-query() does not depend on that setting being automatic. It works from the URI-derived directory structure either way.

Important

You do not need automatic directory-document creation enabled to use cts:directory-query(). Directory-scoped queries operate over URI-derived directory structure, regardless of whether the database directory-creation setting is manual or automatic.

Legacy WebDAV and Slash Documents

Some older systems, especially ones that used WebDAV, may also have real slash-terminated directory documents such as /foo/ and /foo/bar/. In those systems, each slash level is its own URI resource, and that URI can have permissions attached directly to it. Those are not the same thing as the logical directory entries used by cts:directory-query(); they are actual documents stored at directory URIs, typically to carry path-level permissions, properties, or locks.

That distinction matters when reviewing an existing database. If you see documents at every slash, that usually reflects an explicit directory-document model layered on top of the normal URI hierarchy, not a requirement for directory queries to work.

Querying by URI Path

The directory hierarchy implied by URI slashes is directly queryable with cts:directory-query(). Here we query all 3,000 llama profile URIs directly in the leaf directory:

xquery version "1.0-ml";

(: List the URIs of all documents directly inside the llamas directory.  :)
(: Depth "1" matches only documents whose URI has exactly one component   :)
(: below the given prefix — i.e. immediate children.                     :)
(: The llamas directory is a leaf: all 3,000 JSON files sit directly here.:)
cts:uris("", (), cts:directory-query(
  "/cleverllamas/llamaverse/raw/wild-llamas/llamas/",
  "1"
))
/cleverllamas/llamaverse/raw/wild-llamas/llamas/00048384-cb13-4557-805e-a6b4e57f7eab.json
/cleverllamas/llamaverse/raw/wild-llamas/llamas/0005e261-4302-4ae8-9574-732a54040423.json
/cleverllamas/llamaverse/raw/wild-llamas/llamas/000eca10-d166-469f-b17a-3c3b35ee0883.json

Querying at Different Depths

The Depth Parameter

The second argument to cts:directory-query() is the depth:

  • "1" — matches only documents whose URI has exactly one path component below the given prefix. These are the immediate children. Sub-subdirectories and their documents are excluded.
  • "infinity" — matches all documents anywhere under the prefix, at any depth.

Only these two depth values are valid.

The Depth Trap

The most common mistake with directory queries is querying at depth "1" from an intermediate directory that contains no documents directly — only subdirectories. The query returns zero results with no error, and it is not immediately obvious why.

The llamaverse content directory illustrates this exactly. No documents live directly in /cleverllamas/llamaverse/content/ — they are all stored one level deeper in subdirectories (/content/pets/, /content/schools/, /content/animal-shelters/, etc.). Querying at depth "1" therefore returns nothing:

xquery version "1.0-ml";

(: Count documents at two different depths from the content directory.   :)
(: No documents live directly in /content/ — they are one level deeper   :)
(: in subdirectories such as /content/pets/ and /content/schools/.       :)
(: Depth "1" therefore returns 0; "infinity" returns all descendants.    :)
(
  concat("depth 1:        ", count(cts:uris("", (),
    cts:directory-query("/cleverllamas/llamaverse/content/", "1")))),
  concat("depth infinity: ", count(cts:uris("", (),
    cts:directory-query("/cleverllamas/llamaverse/content/", "infinity"))))
)
depth 1:        0
depth infinity: 1261

Switching to "infinity" finds all 1,261 documents in the content subtree. If you need a specific subdirectory level, target that leaf directory directly with "1".

Querying Leaf Directories Directly

When you know the directory where documents live, query it at depth "1". This is the most targeted and efficient form:

xquery version "1.0-ml";

(: Count documents at depth 1 from two leaf directories — animal shelters :)
(: and schools. Both are leaf directories: all their documents sit        :)
(: directly inside them with no further subdirectory nesting.             :)
(
  concat("animal-shelters: ", count(cts:uris("", (),
    cts:directory-query("/cleverllamas/llamaverse/content/animal-shelters/", "1")))),
  concat("schools:         ", count(cts:uris("", (),
    cts:directory-query("/cleverllamas/llamaverse/content/schools/", "1"))))
)
animal-shelters: 15
schools:         6

Legacy and Administrative Cases

Most teams should model around URI structure first and rely on directory-scoped queries over that hierarchy.

If you inherit a system that used WebDAV or another directory-document convention, slash-terminated directory documents may already exist throughout the URI tree. Treat those as stored resources with their own permissions and metadata, not merely as inferred path segments.

Important

The database directory-creation setting and legacy directory documents are administrative concerns. They do not change the core query model: URI structure and slash hierarchy are what give cts:directory-query() its default power.

Directory Deletion

Destructive Operation

xdmp:directory-delete() permanently removes documents in the target subtree. This is destructive and not reversible. Always run the count-first query, validate the affected URIs, and confirm backups before executing deletion in any shared or production environment.

xdmp:directory-delete() takes a directory URI and removes all documents in that subtree recursively. There is no depth argument. The consequences of an accidental deletion are severe — thousands of documents can be removed in a single call with no confirmation prompt.

The safe pattern is always to count or list what will be affected before deleting:

xquery version "1.0-ml";

(: ALWAYS count first before deleting a directory. :)
let $uris := cts:uris("", (), cts:directory-query(
  "/cleverllamas/llamaverse/content/health-records/",
  "infinity"
))
return (
  concat("Documents to be deleted: ", count($uris)),
  $uris
)

(: Review the output. Only when satisfied, run: :)
(: xdmp:directory-delete(                        :)
(:   "/cleverllamas/llamaverse/content/health-records/"  :)
(: )                                              :)

When running xdmp:directory-delete() in production, treat the count step as mandatory. There is no recycle bin and no undo.

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!