
ASHISH PATIL (Customer) asked a question.
Data model: A <catalog-record> contains many repeating <measurement> children (one to several thousand per document). We need to count matching <measurement> entries across the corpus, not the parent documents.
The problem: Every counting mechanism we've tried counts documents (fragments), not sub-elements. cts:estimate, cts:values frequencies, cts:count-aggregate, and search facets all return per-fragment counts. fn:count(cts:search(...)/measurement[...]) gives the correct answer but is too expensive at scale - it materializes every matching node.
Reproducible example:
- <!-- /demo/r1.xml — 2 matching measurements -->
- <catalog-record id="R1">
- <measurement><attribute-name>alpha</attribute-name><value>75</value></measurement>
- <measurement><attribute-name>alpha</attribute-name><value>85</value></measurement>
- <measurement><attribute-name>beta</attribute-name><value>80</value></measurement>
- </catalog-record>
-
- <!-- /demo/r2.xml — 1 matching measurement -->
- <catalog-record id="R2">
- <measurement><attribute-name>alpha</attribute-name><value>88</value></measurement>
- </catalog-record>
Query : attribute-name = alpha AND value between 70–90, scoped to one <measurement>:
- cts:element-query(
- xs:QName("measurement"),
- cts:and-query((
- cts:element-value-query(xs:QName("attribute-name"), "alpha"),
- cts:path-range-query("/catalog-record/measurement/value", ">=", 70.0),
- cts:path-range-query("/catalog-record/measurement/value", "<=", 90.0)
- ))
- )
- Correct answer (matching <measurement> entries) 3
- cts:estimate / any index API returns 2 (documents)
Why chunking is not an option: At production scale this produces ~33 billion tiny sub-KB documents - an unreasonable number of fragments to load, index, and maintain. Fragment-root fragmentation on <measurement> is ruled out for the same reasons.
For completeness' sake, I also tested the same data with TDEs. I made a very simple TDE with one table and one triple:
I tested the table as both virtual and non-virtual with the following SQL:
The virtual version took ~12.5 seconds and the non-virtual version took ~1.5 seconds.
I tested the triple version with the following SPARQL:
This took ~0.5s, making it the performance champ, and the most scalable option if you can afford the indexing time and space (unsurprising, as it is a single range lookup on the triple index)
You may be wondering why I embedded the measurement name in the predicate instead of having two triples that share a single subject. This would be extremely non-performant. Unlike rows, MarkLogic assumes triples are not grouped by any particular fragment. This is because you can easily generate the same subject on multiple documents. Because MarkLogic doesn't know that other matching triples don't exist (potentially on other nodes), it will need to collect all measurement-name triples with an object of "alpha" and all measurement-value triples with an object < 87 and do a join on the E-Node. If you use the Optic API directly, you can get around this by specifying a fragment column in op.fromTriples, but this is still slower than a meaningful predicate and the join isn't completely avoided, just pushed down to the D-Node. You may as well use rows if you really want different triples for name vs value. Dynamic predicate IRIs are a very useful feature of TDEs, and I personally think they are extremely underutilized!