Word Searches and the Optic API
Blend full-text search with structured row logic in one plan
MarkLogic's Optic API lets you blend structured row logic with full-text relevance in the same plan. That is especially useful when search and analytics need to stay in the same workflow rather than being solved separately and joined later.
These examples keep the search predicate, the Optic plan, and the ranked output together so the pattern is easier to reason about. They use cts.wordQuery for clarity, but the same approach works with any cts full-text constructor.
Llamaverse Context for This API Pattern
| Context Item | What this article uses |
|---|---|
| Data set | llamaverse v2.0+ sample data |
| Primary source documents | wild llama JSON documents in the wild-llamas collection |
| Views used | llamaverse.wildLlamas (with optional fragmentId system column when joining) |
| Text target in examples | word matches in document text (for example llama descriptions) |
| Field usage | None in these examples |
| Index context | Text relevance from cts search indexes plus TDE view rows for Optic joins and projections |
If a where(cts.wordQuery(...)) example returns rows but op.fromSearch(...) joins do not, verify the fragmentId columns are exposed on the view side and joined with matching qualifiers.
Source Docs and Configuration Used
| Item | Value used in this article |
|---|---|
| MarkLogic API docs | cts:word-query, op:from-search, op:where |
| Llamaverse source collection | wild-llamas |
| Llamaverse source URI pattern | /cleverllamas/llamaverse/raw/wild-llamas/llamas/{uuid}.json |
| Required database config | TDE view llamaverse.wildLlamas with fragment id support for join examples; text indexes configured for cts:word-query |
What are word queries
MarkLogic provides a strong set of query constructors for words and phrases. In this article we use cts.wordQuery().
cts.wordQuery() matches documents or values containing specific words or phrases. It supports stemming, wildcards, and other search options that materially change result sets.
For the first query, stemming is enabled. That means related forms can match (for example sing, sang, sung, singing). We search for sung and limit display output with fn:subsequence(...) so results stay readable.
{
"name": "Aaron",
"breed": "Huacaya",
"placeOfBirth": "Cusco, Peru",
"secretPowerId": "d8839ba6-2b77-4bcc-9927-b86cdfecb9fb"
}
xquery version "1.0-ml";
fn:subsequence(
cts:search(
cts:and-query((
cts:collection-query("wild-llamas"),
cts:word-query("sang", "stemmed")
))
),
1,
3
)
'use strict';
const results = fn.subsequence(cts.search(cts.andQuery([
cts.collectionQuery('wild-llamas'),
cts.wordQuery('sang', 'stemmed')
])), 1, 3).toArray();
({
sample: 'optic/cts-word-query-and-the-optic-api/assets/basic-word-search.sjs',
kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
count: Array.isArray(results) ? results.length : 0,
data: results
});
Using word-related queries in Optic where clauses
You can use cts.wordQuery(...) inside an Optic where(...) clause to filter rows. This is the core idea: full-text search still resolves against fragments, and Optic can use that signal alongside structured constraints.
This example finds Huacaya llamas where the underlying source content matches sung with stemming enabled. The where clause combines a structured filter on breed with full-text matching. Output is limited to three rows for readability.
{
"name": "Aaron",
"breed": "Huacaya",
"placeOfBirth": "Cusco, Peru",
"secretPowerId": "d8839ba6-2b77-4bcc-9927-b86cdfecb9fb"
}
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
op:from-view("llamaverse", "wildLlamas", "wildLlamas")
=> op:where(op:eq(op:view-col("wildLlamas", "breed"), "Huacaya"))
=> op:where(cts:word-query("sung", "stemmed"))
=> op:limit(3)
=> op:result()
'use strict';
const op = require('/MarkLogic/optic');
const results = op.fromView('llamaverse', 'wildLlamas', 'wildLlamas')
.where(op.eq(op.viewCol('wildLlamas', 'breed'), 'Huacaya'))
.where(cts.wordQuery('sung', 'stemmed'))
.limit(3)
.result();
({
sample: 'optic/cts-word-query-and-the-optic-api/assets/where-word-query.sjs',
kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
count: Array.isArray(results) ? results.length : 0,
data: results
});
Where are records for 'sung'?
This shows the practical effect of stemming. Some matched rows do not contain the literal token sung; they contain related forms such as singing or sang.
The trade-off is visibility into relevance. In where(...), you get filtering but not score. That is where op.fromSearch() helps.
Using op.fromSearch() with cts.wordQuery for scored results
If you need access to the score for each result, you should use the same cts.wordQuery as input to op.fromSearch(). The op.fromSearch() function automatically includes a special score column in the results, allowing you to sort, filter, or display results by relevance.
In this example, we use an inner join to combine op.fromSearch() results with the wildLlamas view. That keeps the Huacaya filter while still preserving text relevance from cts.wordQuery. Results are ordered by descending score so the most relevant rows appear first.
{
"name": "Aaron",
"breed": "Huacaya",
"placeOfBirth": "Cusco, Peru",
"secretPowerId": "d8839ba6-2b77-4bcc-9927-b86cdfecb9fb"
}
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
op:from-search(
cts:and-query((
cts:word-query("sung", "stemmed")
)),
("fragmentId", "score"),
"singingLlamas",
map:entry("scoreMethod", "logtfidf")
)
=> op:join-inner(
op:from-view("llamaverse", "wildLlamas", "wildLlamas", (op:fragment-id-col("fragmentId")))
=> op:where(op:eq(op:view-col("wildLlamas", "breed"), "Huacaya")),
op:on(
op:view-col("singingLlamas", "fragmentId"),
op:view-col("wildLlamas", "fragmentId")
)
)
=> op:order-by(op:desc("score"))
=> op:limit(3)
=> op:result()
'use strict';
const op = require('/MarkLogic/optic');
const results = op.fromSearch(
cts.andQuery([
cts.wordQuery('sung', 'stemmed')
]),
['fragmentId', 'score'],
'singingLlamas',
{ scoreMethod: 'logtfidf' }
)
.joinInner(
op.fromView('llamaverse', 'wildLlamas', 'wildLlamas', [op.fragmentIdCol('fragmentId')])
.where(op.eq(op.viewCol('wildLlamas', 'breed'), 'Huacaya')),
op.on(op.viewCol('singingLlamas', 'fragmentId'), op.viewCol('wildLlamas', 'fragmentId'))
)
.orderBy(op.desc('score'))
.limit(3)
.result();
({
sample: 'optic/cts-word-query-and-the-optic-api/assets/from-search-word-query.sjs',
kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
count: Array.isArray(results) ? results.length : 0,
data: results
});
The returned rows show how score differentiates otherwise similar matches. Treat this as your decision point: use where(...) for strict filtering, and use op.fromSearch() when ranking needs to be visible and explainable.
When to Combine cts:word-query with Optic
Combine cts.wordQuery with Optic when you need both text search and structured row shaping in one plan.
Choose where(...) for strict inclusion and exclusion logic. Choose op.fromSearch() when ranked results with visible scores matter. Document that choice once and search behaviour stays predictable under pressure.
- CTS Text Query Building Starter Pack — Master the
ctsquery building blocks before combining them with Optic. - Optic Data Source Starter Pack — See
op.fromSearch()alongside other Optic source shapes. - CTS Relevance and Scoring Starter Pack — Understand relevance scoring before ranking through Optic.
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!
- Llamaverse Context for This API Pattern
- Source Docs and Configuration Used
- What are word queries
- Where are records for 'sung'?
- Using op.fromSearch() with cts.wordQuery for scored results
- When to Combine cts:word-query with Optic