The MarkLogic Optic API makes it possible to perform relational operations on indexed values and documents. The Optic API is not a single API, but rather a set of APIs exposed within the XQuery, JavaScript, and Java languages.
The Optic API can read any indexed value, whether the value is in a range index, the triple index, or rows extracted by a template. The extraction templates, such as those used to create template views described in Creating Template Views in the SQL Data Modeling Guide, are a simple, powerful way to specify a relational lens over documents, making parts of your document data accessible via SQL. Optic gives you access to the same relational operations, such as joins and aggregates, over rows. The Optic API also enables document search to match rows projected from documents, joined documents as columns within rows, and dynamic document structures, all performed efficiently within the database and accessed programmatically from your application.
The Optic API allows you to use your data as-is and makes it possible to make use of MarkLogic document and search features using JavaScript or XQuery syntax, incorporating common SQL concepts, regardless of the structure of your data. Unlike SQL, Optic is well suited for building applications and accessing the full range of MarkLogic NoSQL capabilities. Because Optic is integrated into common application languages, it can perform queries within the context of broader applications that perform updates to data and process results for presentation to end users.
As in the SQL and SPARQL interfaces, you can use the Optic API to build a query from standard operations such as where
, groupBy
, orderBy
, union
, and join
but by expressing the operations through calls to JavaScript and XQuery functions. The Optic API enables you to work in the environment of the programming language, taking advantage of variables and functions for benefits such as modularizing plan construction and avoiding the parse errors and injection attacks associated with assembling a query by concatenating strings.
Unlike in SQL, column order is indeterminate in Optic. Notable exceptions of the sort order keys in orderby
and grouping keys in groupby
, which specify priority.
There is also an Optic Java Client API, which is described in Optic Java API for Relational Operations in the Developing Applications With the Java Client API guide.
This chapter has the following main sections:
The XQuery Optic API and JavaScript Optic API are functionally equivalent. Each is adapted to the features and practices of their respective language conventions, but otherwise both are as consistent as possible and have the same performance profile. You should use the language that best suits your skills and programming environment.
The table below highlights the differences between the JavaScript and XQuery versions of the Optic API.
Characteristic | JavaScript | XQuery |
---|---|---|
Namespaces for proxy functions | Nested namespaces (such as op.fn.min ) |
A module in a separate namespace conforming to the following template (for a prefix, such as
For details, see XQuery Libraries Required for Expression Functions. |
Fluent object chaining | Methods that return objects | Functions take a state object as the first parameter and return a state object, enabling use of the XQuery => chaining operator. These black-box objects hold the state of the plan being built in the form of a map. Because these state objects might change in a future release, they should not be modified, serialized or persisted. Chained functions always create a new map instead of modifying the existing map. |
Naming convention | camelCase | Hyphen-separated naming convention with the exception of proxy functions for a camelcase original function (such as the fn:current-dateTime function). |
Unbounded parameters | Allowed | Supported as a single sequence parameter. The sole examples at present are the proxy functions for fn:concat and sem:coalesce. |
Result types | Returns a sequence of objects, with the option to return a sequence of arrays | Returns a map of sql:rows , with the option to return an array consisting of a header and rows. |
The following graphic illustrates the objects that are used as input and output by the methods in an Optic pipeline.
An Optic query creates a pipeline that applies a sequence of relational operations to a row set. The following are the basic characteristics of the functions and methods used in an Optic query:
from*
function) produce an output row set in the form of an AccessPlan
object.ModifyPlan
object.ModifyPlan
object.The following is simple example that selects specific columns from the rows in a view and outputs them in a particular order. The pipeline created by this query is illustrated below.
const op = require('/MarkLogic/optic'); op.fromView('main', 'employees') .select(['EmployeeID', 'FirstName', 'LastName']) .orderBy('EmployeeID') .result();
AccessPlan
object that can be used by all of the API methods. ModifyPlan
object. ModifyPlan
object.ModifyPlan
object and executes the plan.The following example calculates the total expenses for each employee and returns the results in order of employee number.
const op = require('/MarkLogic/optic'); const employees = op.fromView('main', 'employees'); const expenses = op.fromView('main', 'expenses'); const Plan = employees.joinInner(expenses, op.on(employees.col('EmployeeID'), expenses.col('EmployeeID'))) .groupBy(employees.col('EmployeeID'), ['FirstName','LastName', op.sum('totalexpenses', expenses.col('Amount'))]) .orderBy('EmployeeID') Plan.result();
The absence of .select
is equivalent to a SELECT *
in SQL, retrieving all columns in a view.
AccessPlan
objects that are used by the op.on function and AccessPlan.prototype.col methods to direct the ModifyPlan.prototype.joinInner method to join the row sets from both views, which then ouputs them as a single row set in the form of a ModifyPlan
object. ModifyPlan
object.ModifyPlan
object and executes the plan.The following functions access data indexed as rows, triples, and lexicons, as well as literal row sets constructed in the program:
JavaScript | XQuery |
---|---|
op.fromView | op:from-view |
op.fromTriples | op:from-triples |
op.fromLiterals | op:from-literals |
op.fromLexicons | op:from-lexicons |
op.fromSQL | op:from-sql |
op.fromSPARQL | op:from-sparql |
The op.fromView function accesses indexes created by a template view, as described in Creating Template Views in the SQL Data Modeling Guide.
The op.fromTriples
function accesses semantic triple indexes and abstracts them as rows and columns. Note, however, that the columns of rows from an RDF graph may have varying data types, which could affect joins.
The op.fromLexicons
function dynamically constructs a view with columns on range-indexes, URI lexicons, and collection lexicons. Lexicons are often joined to enrich data indexed in views. Accessing lexicons from Optic may be useful if your application already has range indexes defined, or if URI or collection information is required for your query.
The op.fromLiterals
function constructs a literal row set that is similar to the results from a SQL VALUES or SPARQL VALUES statement. This allows you to provide alternative columns to join with an existing view.
The op.fromSQL and op.fromSPARQL
functions dynamically construct a row set based on a SELECT queries template views and triples, respectively
The following sections provide examples of the different data access functions:
Queries using fromView
retrieve indexed rows exposed over documents. The examples in this section are based on documents and template views described in the SQL on MarkLogic Server Quick Start chapter in the SQL Data Modeling Guide.
List all of the employees in order of ID number.
const op = require('/MarkLogic/optic'); op.fromView('main', 'employees') .select(['EmployeeID', 'FirstName', 'LastName']) .orderBy('EmployeeID') .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:from-view("main", "employees") => op:select(("EmployeeID", "FirstName", "LastName")) => op:order-by("EmployeeID") => op:result()
You can use Optic to filter rows for specific data of interest. For example, the following query returns the ID and name for employee 3.
const op = require('/MarkLogic/optic'); op.fromView('main', 'employees') .where(op.eq(op.col('EmployeeID'), 3)) .select(['EmployeeID', 'FirstName', 'LastName']) .orderBy('EmployeeID') .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:from-view("main", "employees") => op:where(op:eq(op:col("EmployeeID"), 3)) => op:select(("EmployeeID", "FirstName", "LastName")) => op:order-by("EmployeeID") => op:result()
The following query returns all of the expenses and expense categories for each employee and return results in order of employee number. Because some information is contained only on the expense reports and some data is only in the employee record, a row join on EmployeeID
is used to pull data from both sets of documents and produce a single, integrated row set.
const op = require('/MarkLogic/optic'); const employees = op.fromView('main', 'employees'); const expenses = op.fromView('main', 'expenses'); const Plan = employees.joinInner(expenses, op.on(employees.col('EmployeeID'), expenses.col('EmployeeID'))) .select([employees.col('EmployeeID'), 'FirstName', 'LastName', 'Category', 'Amount']) .orderBy(employees.col('EmployeeID')) Plan.result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $employees := op:from-view("main", "employees") let $expenses := op:from-view("main", "expenses") return $employees => op:join-inner($expenses, op:on( op:view-col("employees", "EmployeeID"), op:view-col("expenses", "EmployeeID"))) => op:select((op:view-col("employees", "EmployeeID"), "FirstName", "LastName", "Category", "Amount")) => op:order-by(op:view-col("employees", "EmployeeID")) => op:result()
Locate employee expenses that exceed the allowed limit. The where
operation in this example demonstrates the nature of the Optic chaining pipeline, as it applies to all of the preceding rows.
const op = require('/MarkLogic/optic'); const employees = op.fromView('main', 'employees'); const expenses = op.fromView('main', 'expenses'); const expenselimit = op.fromView('main', 'expenselimit'); const Plan = employees.joinInner(expenses, op.on(employees.col('EmployeeID'), expenses.col('EmployeeID'))) .joinInner(expenselimit, op.on(expenses.col('Category'), expenselimit.col('Category'))) .where(op.gt(expenses.col('Amount'), expenselimit.col('Limit'))) .select([employees.col('EmployeeID'), 'FirstName', 'LastName', expenses.col('Category'), expenses.col('Amount'), expenselimit.col('Limit') ]) .orderBy(employees.col('EmployeeID')) Plan.result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $employees := op:from-view("main", "employees") let $expenses := op:from-view("main", "expenses") let $expenselimit := op:from-view("main", "expenselimit") return $employees => op:join-inner($expenses, op:on( op:view-col("employees", "EmployeeID"), op:view-col("expenses", "EmployeeID"))) => op:join-inner($expenselimit, op:on( op:view-col("expenses", "Category"), op:view-col("expenselimit", "Category"))) => op:where(op:gt(op:view-col("expenses", "Amount"), op:view-col("expenselimit", "Limit"))) => op:select((op:view-col("employees", "EmployeeID"), "FirstName", "LastName", op:view-col("expenses", "Category"), op:view-col("expenses", "Amount"), op:view-col("expenselimit", "Limit"))) => op:order-by(op:view-col("employees", "EmployeeID")) => op:result()
The following example returns a list of the people who were born in Brooklyn in the form of a table with two columns, person
and name
. This is executed against the example dataset described in Loading Triples in the Semantics Developer's Guide.
const op = require('/MarkLogic/optic'); // prefixer is a factory for sem:iri() constructors in a namespace const resource = op.prefixer('http://dbpedia.org/resource/'); const foaf = op.prefixer('http://xmlns.com/foaf/0.1/'); const onto = op.prefixer('http://dbpedia.org/ontology/'); const person = op.col('person'); const Plan = op.fromTriples([ op.pattern(person, onto('birthPlace'), resource('Brooklyn')), op.pattern(person, foaf("name"), op.col("name")) ]) Plan.result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $resource := op:prefixer("http://dbpedia.org/resource/") let $foaf := op:prefixer("http://xmlns.com/foaf/0.1/") let $onto := op:prefixer("http://dbpedia.org/ontology/") let $person := op:col("person") return op:from-triples(( op:pattern($person, $onto("birthPlace"), $resource("Brooklyn")), op:pattern($person, $foaf("name"), op:col("name")))) => op:result()
The fromLexicons
function may be useful if you already have range indexes defined for use elsewhere in your application. This data access function enables you to incorporate lexicons as another source of data for your query pipeline.
The examples in this section operate on the documents described in Load the Data in the SQL Data Modeling Guide.
The fromLexicons
function queries on range index names, rather than column names in a view. For example, for the employee
documents, rather than query on EmployeeID
, you create a range index, named ID
, and query on ID
.
First, in the database holding your data, create element range indexes for the following elements: ID
, Position
, FirstName
, and LastName
. For details on how to create range indexes, see Defining Element Range Indexes in the Administrator's Guide.
The following example returns the EmployeeID
for each employee. The text, myview
, is prepended to each column name.
const op = require('/MarkLogic/optic'); const Plan = op.fromLexicons( {EmployeeID: cts.elementReference(xs.QName('ID'))}); Plan.result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:from-lexicons( map:entry( "EmployeeID", cts:element-reference(xs:QName("ID"))), "myview") => op:result()
The following example returns the EmployeeID
, FirstName
, LastName
, and the URI of the document holding the data for each employee.
const op = require('/MarkLogic/optic'); const Plan = op.fromLexicons({ EmployeeID: cts.elementReference(xs.QName('ID')), FirstName: cts.elementReference(xs.QName('FirstName')), LastName: cts.elementReference(xs.QName('LastName')), URI: cts.uriReference()}); Plan.result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:from-lexicons( map:entry("EmployeeID", cts:element-reference(xs:QName("ID"))) => map:with("FirstName", cts:element-reference(xs:QName("FirstName"))) => map:with("LastName", cts:element-reference(xs:QName("LastName"))) => map:with("uri", cts:uri-reference())) => op:result()
Every view contains a fragment ID. The fragment ID generated from op.fromLexicons can be used to join with the
fragment ID of a view. For example, the following returns the EmployeeID
, FirstName
, LastName
, Position
, and document URI
for each employee.
const op = require('/MarkLogic/optic'); const empldocid = op.fragmentIdCol('empldocid'); const uridocid = op.fragmentIdCol('uridocid'); const employees = op.fromView('main', 'employees', null, empldocid); const DFrags = op.fromLexicons({'URI': cts.uriReference()}, null, uridocid) const Plan = employees.joinInner(DFrags, op.on(empldocid, uridocid)) .select(['URI', 'EmployeeID', 'FirstName', 'LastName', 'Position']); Plan.result() ;
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $empldocid := op:fragment-id-col("empldocid") let $uridocid := op:fragment-id-col("uridocid") let $employees := op:from-view("main", "employees", (), $empldocid) let $DFrags := op:from-lexicons(map:entry("URI", cts:uri-reference()), (), $uridocid) return $employees => op:join-inner($DFrags, op:on($empldocid, $uridocid)) => op:select((op:view-col("employees", "EmployeeID"), ("URI", "FirstName", "LastName", "Position"))) => op:result()
The fromLiterals
function enables you to dynamically generate rows based on run-time input of arrays and objects of strings. This data access function is helpful for testing and debugging.
Build a table with two rows and return the row that matches the id
column value of 1:
const op = require('/MarkLogic/optic'); op.fromLiterals([ {id:1, name:'Master 1', date:'2015-12-01'}, {id:2, name:'Master 2', date:'2015-12-02'} ]) .where(op.eq(op.col('id'),1)) .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:from-literals( map:entry("columnNames", json:to-array(("id", "name", "date"))) => map:with("rowValues", ( json:to-array(( 1, "Master 1", "2015-12-01")), json:to-array(( 2, "Master 2", "2015-12-02"))))) => op:where(op:eq(op:col("id"), 1)) => op:result()
Build a table with five rows and return the average values for group
1
and group
2
:
const op = require('/MarkLogic/optic'); op.fromLiterals([ {group:1, val:2}, {group:1, val:4}, {group:2, val:3}, {group:2, val:5}, {group:2, val:7} ]) .groupBy('group', op.avg('valAvg', 'val')) .orderBy('group') .result()
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:from-literals(( map:entry("group", 1) => map:with("val", 2), map:entry("group", 1) => map:with("val", 4), map:entry("group", 2) => map:with("val", 3), map:entry("group", 2) => map:with("val", 5), map:entry("group", 2) => map:with("val", 7) )) => op:group-by("group", op:avg("valAvg", "val")) => op:order-by("group") => op:result()
The fromSQL
function enables you to dynamically generate rows based on a SQL SELECT query from template views.
List all of the employees in the employees view:
const op = require('/MarkLogic/optic'); op.fromSQL('SELECT employees.FirstName, employees.LastName \ FROM employees') .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:from-sql('SELECT employees.FirstName, employees.LastName FROM employees') => op:result()
The fromSPARQL
function enables you to dynamically generate rows based on a SPARQL SELECT query from triples.
List all of the people born in Brooklyn:
const op = require('/MarkLogic/optic'); op.fromSPARQL('SELECT employees.FirstName, employees.LastName \ FROM employees') .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:from-sparql('PREFIX db: <http://dbpedia.org/resource/> PREFIX foaf: <http://xmlns.com/foaf/0.1/> PREFIX onto: <http://dbpedia.org/ontology/> SELECT ?person ?name WHERE { ?person onto:birthPlace db:Brooklyn; foaf:name ?name .}') => op:result()
This section describes some of the kinds of Optic queries. The examples in this section are based on documents and template views described in the SQL on MarkLogic Server Quick Start chapter in the SQL Data Modeling Guide.
Begin using the Optic API by performing a basic query on a view over documents. Querying the view will return rows.
For example, the following lists all of the employee IDs and names in order of ID number.
const op = require('/MarkLogic/optic'); op.fromView('main', 'employees') .select(['EmployeeID', 'FirstName', 'LastName']) .orderBy('EmployeeID') .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:from-view("main", "employees") => op:select(("EmployeeID", "FirstName", "LastName")) => op:order-by("EmployeeID") => op:result()
Use the MarkLogic Optic API to conveniently perform aggregate functions on values across documents. The following examples perform several operations to get a sense of basic statistics about employee expenses. For information on the op.math.trunc
and omath:trunc
proxy functions used in these examples, see Expression Functions For Processing Column Values.
Grouping in Optic differs from SQL. In SQL, the grouping keys are in the GROUP BY statement and the aggregates are separately declared in the SELECT. In an Optic group-by operation, the grouping keys are the first parameter and the aggregates are an optional second parameter. In this way, Optic enables you to aggregate sequences and arrays in a group-by operation and then call expression functions that operate on these sequences and arrays. For example, many of the math:*
functions, described in Expression Functions For Processing Column Values, take a sequence.
In Optic, instead of applying aggregate functions to the group, a simple column can be supplied. Optic will sample the value of the column for one arbitrary row within the group. This can be useful when the column has the same value in every row within the group; for example, when grouping on a department number but sampling on the department name.
const op = require('/MarkLogic/optic'); op.fromView('main', 'expenses') .groupBy(null, [ op.count('ExpenseReports', 'EmployeeID'), op.min('minCharge', 'Amount'), op.avg('average', 'Amount'), op.max('maxCharge', 'Amount') ]) .select(['ExpenseReports', 'minCharge', op.as('avgCharge', op.math.trunc(op.col('average'))), 'maxCharge']) .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; import module namespace omath="http://marklogic.com/optic/expression/math" at "/MarkLogic/optic/optic-math.xqy"; let $expenses := op:from-view("main", "expenses") return $expenses => op:group-by((), ( op:count("ExpenseReports", "EmployeeID"), op:min("minCharge", "Amount"), op:avg("average", "Amount"), op:max("maxCharge", "Amount") )) => op:select(("ExpenseReports", "minCharge", op:as("avgCharge", omath:trunc(op:col("average"))), "maxCharge")) => op:result();
Optic supports the following types of row joins:
Method | Description |
---|---|
joinInner | Creates one output row set that concatenates one left row and one right row for each match between the keys in the left and right row sets. |
joinLeftOuter | Creates one output row set with all of the rows from the left row set with the matching rows in the right row set, or NULL when there is no match. |
joinCrossProduct | Creates one output row set that concatenates every left row with every right row. |
The examples in this section join the employees and expenses views to return more information on employee expenses and their categories than what is available on individual documents.
The following queries make use of the AccessPlan.prototype.joinInner and op:join-inner functions to return all of the expenses and expense categories for each employee in order of employee number. The join will supplement employee
data with information stored in separate expenses
documents. The inner join acts as a filter and will only include those employees
with expenses
.
const op = require('/MarkLogic/optic'); const employees = op.fromView('main', 'employees'); const expenses = op.fromView('main', 'expenses'); const Plan = employees.joinInner(expenses, op.on(employees.col('EmployeeID'), expenses.col('EmployeeID'))) .select([employees.col('EmployeeID'), 'FirstName', 'LastName', expenses.col('Category'), 'Amount']) .orderBy(employees.col('EmployeeID')) Plan.result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $employees := op:from-view("main", "employees") let $expenses := op:from-view("main", "expenses") return $employees => op:join-inner($expenses, op:on( op:view-col("employees", "EmployeeID"), op:view-col("expenses", "EmployeeID"))) => op:select((op:view-col("employees", "EmployeeID"), "FirstName", "LastName", "Category")) => op:order-by(op:view-col("employees", "EmployeeID")) => op:result()
Use the AccessPlan.prototype.where and op:where functions to locate employee expenses that exceed the allowed limit. Join the employees
, expenses
, and category
limits to get a 360 degree view of employee expenses.
const op = require('/MarkLogic/optic'); const employees = op.fromView('main', 'employees'); const expenses = op.fromView('main', 'expenses'); const expenselimit = op.fromView('main', 'expenselimit'); const Plan = employees.joinInner(expenses, op.on(employees.col('EmployeeID'), expenses.col('EmployeeID'))) .joinInner(expenselimit, op.on(expenses.col('Category'), expenselimit.col('Category'))) .where(op.gt(expenses.col('Amount'), expenselimit.col('Limit'))) .select([employees.col('EmployeeID'), 'FirstName', 'LastName', expenses.col('Category'), expenses.col('Amount'), expenselimit.col('Limit') ]) .orderBy(employees.col('EmployeeID')) Plan.result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $employees := op:from-view("main", "employees") let $expenses := op:from-view("main", "expenses") let $expenselimit := op:from-view("main", "expenselimit") return $employees => op:join-inner($expenses, op:on( op:view-col("employees", "EmployeeID"), op:view-col("expenses", "EmployeeID"))) => op:join-inner($expenselimit, op:on( op:view-col("expenses", "Category"), op:view-col("expenselimit", "Category"))) => op:where(op:gt(op:view-col("expenses", "Amount"), op:view-col("expenselimit", "Limit"))) => op:select((op:view-col("employees", "EmployeeID"), "FirstName", "LastName", op:view-col("expenses", "Category"), op:view-col("expenses", "Amount"), op:view-col("expenselimit", "Limit"))) => op:order-by(op:view-col("employees", "EmployeeID")) => op:result()
The following queries make use of the AccessPlan.prototype.joinLeftOuter and op:join-left-outer functions to return all of the expenses and expense categories for each employee in order of employee number, or null values for employees without matching expense records.
const op = require('/MarkLogic/optic');
const employees = op.fromView('main', 'employees');
const expenses = op.fromView('main', 'expenses');
const Plan =
employees.joinLeftOuter
(expenses, op.on(employees.col('EmployeeID'),
expenses.col('EmployeeID')))
.orderBy(employees.col('EmployeeID'))
Plan.result();
xquery version "1.0-ml";
import module namespace op="http://marklogic.com/optic"
at "/MarkLogic/optic.xqy";
let $employees := op:from-view("main", "employees")
let $expenses := op:from-view("main", "expenses")
return $employees
=> op:join-left-outer
($expenses, op:on(
op:view-col("employees", "EmployeeID"),
op:view-col("expenses", "EmployeeID")))
=> op:order-by(op:view-col("employees", "EmployeeID"))
=> op:result()
The following queries make use of the AccessPlan.prototype.joinCrossProduct and op:join-cross-product functions to return all of the expenses and expense categories for each employee title (Position) in order of expense Category. If employees with a particular position do not have any expenses under a category, the reported expense is 0.
const op = require('/MarkLogic/optic'); const employees = op.fromView('main', 'employees'); const expenses = op.fromView('main', 'expenses'); expenses.groupBy ('Category') .joinCrossProduct(employees.groupBy('Position')) .select(null, 'all') .joinLeftOuter( expenses.joinInner(employees, op.on(employees.col('EmployeeID'), expenses.col('EmployeeID')) ) .groupBy(['Category', 'Position'], op.sum('rawExpense', expenses.col('Amount')) ) .select(null, 'expensed'), [op.on(op.viewCol('expensed', 'Category'), op.viewCol('all', 'Category')), op.on(op.viewCol('expensed', 'Position'), op.viewCol('all', 'Position'))] ) .select([op.viewCol('all', 'Category'), op.viewCol('all', 'Position'), op.as('expense', op.sem.coalesce(op.col('rawExpense'), 0)) ]) .orderBy(['Category', 'Position']) .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; import module namespace osem="http://marklogic.com/optic/expression/sem" at "/MarkLogic/optic/optic-sem.xqy"; let $employees := op:from-view("main", "employees") let $expenses := op:from-view("main", "expenses") let $rawExpense := op:col("rawExpense") return $expenses => op:group-by('Category') => op:join-cross-product($employees => op:group-by("Position")) => op:select((), 'all') => op:join-left-outer( $expenses => op:join-inner($employees, op:on( op:col($employees, "EmployeeID"), op:col($expenses, "EmployeeID") )) => op:group-by(("Category", "Position"), op:sum("rawExpense", op:col($expenses, "Amount"))) => op:select((), "expensed"), (op:on(op:view-col("expensed", "Category"), op:view-col("all", "Category")), op:on(op:view-col("expensed", "Position"), op:view-col("all", "Position"))) ) => op:select((op:view-col("all", "Category"), op:view-col("all", "Position"), op:as("expense", osem:coalesce((op:col("rawExpense"), 0))))) => op:order-by(("Category", "Position")) => op:result();
The Optic API provides access not only to rows within views, but also to documents themselves.
Optic support the following types of document joins:
Method | Description |
---|---|
joinDoc | Joins the source documents for rows (especially when the source documents have detail that's not projected into rows). In this case, you should name the fragment id column and use it in the join |
joinDocUri | Joins related documents based on document URIs. The AccessPlan.prototype.joinDocUri method provides a convenient way to join documents by their URIs. However, if you need more control (for example, left outer joins on related documents), you can use the explicit join with the cts.uriReference lexicon to get the fragment id and join the documents on the fragment id. After joining documents, you can use the op.xpath function to project or an xdmp:* function to add columns with the metadata for documents. |
You should minimize the number of documents retrieved by filtering or limiting rows before joining documents.
In the examples below, the 'employee' and 'expense' source documents are returned by the AccessPlan.prototype.joinDoc or op:join-doc function after the row data. The join is done on the document fragment ids returned by op.fromView.
const op = require('/MarkLogic/optic'); const empldocid = op.fragmentIdCol('empldocid'); const expdocid = op.fragmentIdCol('expdocid'); const employees = op.fromView('main', 'employees', null, empldocid); const expenses = op.fromView('main', 'expenses', null, expdocid); const Plan = employees.joinInner(expenses, op.on(employees.col('EmployeeID'), expenses.col('EmployeeID'))) .joinDoc('Employee', empldocid) .joinDoc('Expenses', expdocid) .select([employees.col('EmployeeID'),'FirstName', 'LastName', expenses.col('Category'), 'Amount', 'Employee', 'Expenses']) .orderBy(employees.col('EmployeeID')) Plan.result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $empldocid := op:fragment-id-col("empldocid") let $expdocid := op:fragment-id-col("expdocid") let $employees := op:from-view("main", "employees", (), $empldocid) let $expenses := op:from-view("main", "expenses", (), $expdocid) return $employees => op:join-inner($expenses, op:on( op:view-col("employees", "EmployeeID"), op:view-col("expenses", "EmployeeID"))) => op:join-doc("Employee", $empldocid) => op:join-doc("Expenses", $expdocid) => op:select((op:view-col("employees", "EmployeeID"), "FirstName", "LastName", op:view-col("expenses", "Category"), op:view-col("expenses", "Amount"), "Employee", "Expenses")) => op:order-by(op:view-col("employees", "EmployeeID")) => op:result()
The following examples show how the AccessPlan.prototype.joinDocUri or op:join-doc-uri function can be used to return the document URI along with the row data.
const op = require('/MarkLogic/optic'); const empldocid = op.fragmentIdCol('empldocid'); const employees = op.fromView('main', 'employees', null, empldocid); employees.joinDocUri(op.col('uri'), empldocid) .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $empldocid := op:fragment-id-col("empldocid") return op:from-view("main", "employees", (), $empldocid) => op:join-doc-uri(op:col("uri"), $empldocid) => op:result()
Optic supports the following ways to combine data into new rows:
Method | Description |
---|---|
union | Combines all of the rows from the input row sets. Columns that are present only in some input row sets effectively have a null value in the rows from the other row sets. |
intersect | Creates one output row set from the rows that have the same columns and values in both the left and right row sets. |
except | Creates one output row set from the rows that have the same columns in both the left and right row sets, but the column values in the left row set do not match the column values in the right row set. |
The examples in this section operate on the employees and expenses views to return more information on employee expenses and their categories than what is available on individual documents.
The following queries make use of the AccessPlan.prototype.union and op:union functions to return all of the expenses and expense categories for each employee in order of employee number.
const op = require('/MarkLogic/optic');
const employees = op.fromView('main', 'employees');
const expenses = op.fromView('main', 'expenses');
const Plan =
employees.union(expenses)
.whereDistinct()
.orderBy([employees.col('EmployeeID')])
Plan.result();
xquery version "1.0-ml";
import module namespace op="http://marklogic.com/optic"
at "/MarkLogic/optic.xqy";
let $employees := op:from-view("main", "employees")
let $expenses := op:from-view("main", "expenses")
return $employees
=> op:union($expenses)
=> op:where-distinct()
=> op:order-by(op:view-col("employees", "EmployeeID"))
=> op:result()
The following queries make use of the AccessPlan.prototype.intersect and op:intersect
functions to return the matching columns and values in the tables, tab1
and tab2
.
The op.fromLiterals function is used for this example because the data set does not contain redundant columns and values.
const op = require('/MarkLogic/optic'); const tab1 = op.fromLiterals([ {id:1, val:'a'}, {id:2, val:'b'}, {id:3, val:'c'} ]); const tab2 = op.fromLiterals([ {id:1, val:'x'}, {id:2, val:'b'}, {id:3, val:'c'} ]); tab1.intersect(tab2) .orderBy('id') .result();
xquery version "1.0-ml";
import module namespace op="http://marklogic.com/optic"
at "/MarkLogic/optic.xqy";
let $tab1 := op:from-literals((
map:entry("id", 1) => map:with("val", "a"),
map:entry("id", 2) => map:with("val", "b"),
map:entry("id", 3) => map:with("val", "c")
))
let $tab2 := op:from-literals((
map:entry("id", 1) => map:with("val", "x"),
map:entry("id", 2) => map:with("val", "b"),
map:entry("id", 3) => map:with("val", "c")
))
return $tab1
=> op:intersect($tab2)
=> op:order-by("id")
=> op:result()
The following queries make use of the AccessPlan.prototype.except and op:except functions to return the columns and values in tab1
that do not match those in tab2
.
The op.fromLiterals function is used for this example because the data set does not contain redundant columns and values.
const op = require('/MarkLogic/optic'); const tab1 = op.fromLiterals([ {id:1, val:'a'}, {id:2, val:'b'}, {id:3, val:'c'} ]); const tab2 = op.fromLiterals([ {id:1, val:'x'}, {id:2, val:'b'}, {id:3, val:'c'} ]); tab1.except(tab2) .orderBy('id') .result();
xquery version "1.0-ml";
import module namespace op="http://marklogic.com/optic"
at "/MarkLogic/optic.xqy";
let $tab1 := op:from-literals((
map:entry("id", 1) => map:with("val", "a"),
map:entry("id", 2) => map:with("val", "b"),
map:entry("id", 3) => map:with("val", "c")
))
let $tab2 := op:from-literals((
map:entry("id", 1) => map:with("val", "x"),
map:entry("id", 2) => map:with("val", "b"),
map:entry("id", 3) => map:with("val", "c")
))
return $tab1
=> op:except($tab2)
=> op:order-by("id")
=> op:result()
The MarkLogic Optic API can be combined with other types of queries. Developers can restrict rows based on a document query, even if there are parts of the document that are not part of the row view. The following demonstrates the use of the AccessPlan.prototype.where and op:where functions to express a document query within the Optic API:
const op = require('/MarkLogic/optic'); op.fromView('main', 'employees') .where(cts.andQuery([cts.wordQuery('Senior'), cts.wordQuery('Researcher')])) .select(['FirstName', 'LastName', 'Position']) .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $employees := op:from-view("main", "employees") return $employees => op:where(cts:and-query((cts:word-query("Senior"), cts:word-query("Researcher")))) => op:select(("FirstName", "LastName", "Position")) => op:result()
Optic JavaScript queries in Query Console output results in the form of serialized JSON objects. In most cases, you will want to have some code that consumes the Optic output. For example, the following query maps the Optic output to an HTML table.
const op = require('/MarkLogic/optic'); let keys = null; const rowItr = op.fromView('main', 'employees') .map(row => { if (keys === null) { keys = Object.keys(row); } return `<tr>${keys.map(key => `<td>${row[key]}</td>`)}</tr>`; }) .result(); const rows = Array.from(rowItr).join('\n'); const header = `<tr>${keys.map(key => `<th>${key}</th>`)}</tr>`; const report = `<table>\n${header}\n${rows}\n</table>`; report;
To view the output as a table in Query Console, select HTML
from the String as
menu.
Optic supports expression functions that represent builtin functions to process column values returned by op.col and op:col. These include datatype constructors, datetime, duration, numeric, sequence, and string functions. Expression functions are both
For example, the math.trunc function is expressed by the op.math.trunc
expression function in JavaScript and as omath:trunc
in XQuery.
For example, the truncate to decimal portion of the returned 'average' value, do the following:
op.math.trunc(op.col('average')) // JavaScript omath:trunc(op:col('average')) (: XQuery :)
The list of JavaScript functions supported by expression functions is shown in the table below. Their XQuery equivalents are also supported, but you must import the respective module libraries listed in XQuery Libraries Required for Expression Functions.
Most every value processing built-in function you would want to use is listed below. In the unlikely event that you want to call a function that is not listed, the Optic API provides a general-purpose op.call constructor for deferred calls:
op.call(moduleUri, functionName, arg*) => expression op.call({uri:..., name:..., args:*}) => expression
Use the op.call function with care because some builtins could adversely affect performance or worse. You cannot call JavaScript or XQuery functions using this function. Instead, provide a map or reduce function to postprocess the results.
In XQuery, the following libraries must be imported to use the expression functions for the respective built-in functions.
import module namespace octs="http://marklogic.com/optic/expression/cts" at "/MarkLogic/optic/optic-cts.xqy";
import module namespace ofn="http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";
import module namespace ojson="http://marklogic.com/optic/expression/json" at "/MarkLogic/optic/optic-json.xqy";
import module namespace omap="http://marklogic.com/optic/expression/map" at "/MarkLogic/optic/optic-map.xqy";
import module namespace omath="http://marklogic.com/optic/expression/math" at "/MarkLogic/optic/optic-math.xqy";
import module namespace ordf="http://marklogic.com/optic/expression/rdf" at "/MarkLogic/optic/optic-rdf.xqy";
import module namespace osem="http://marklogic.com/optic/expression/sem" at "/MarkLogic/optic/optic-sem.xqy";
import module namespace ospell="http://marklogic.com/optic/expression/spell" at "/MarkLogic/optic/optic-spell.xqy";
import module namespace osql="http://marklogic.com/optic/expression/sql" at "/MarkLogic/optic/optic-sql.xqy";
import module namespace oxdmp="http://marklogic.com/optic/expression/xdmp" at "/MarkLogic/optic/optic-xdmp.xqy";
import module namespace oxs="http://marklogic.com/optic/expression/xs" at "/MarkLogic/optic/optic-xs.xqy";
Expression functions can be nested for powerful expressions that transform values. For example:
.select(['countUsers', 'minReputation', op.as('avgReputation', op.math.trunc(op.col('aRep'))), 'maxReputation', op.as('locationPercent', op.fn.formatNumber(op.xs.double( op.divide(op.col('locationCount'), op.col('countUsers'))),'##%')) ])
Function | SPARQL | SQL | Comments |
---|---|---|---|
= | In expressions, the call should pass a op.col value to identify a column. | ||
> | > | ||
>= | >= | ||
< | < | ||
<= | <= | ||
!= | != | ||
&& | AND | ||
|| | OR | ||
! | NOT | ||
IF | CASE WHEN ELSE | ||
WHEN | |||
BOUND | IS NULL | ||
add(numericExpression, numericExpression) => numericExpression |
+ | + | A column must be named with an op.col value. |
divide(numericExpression, numericExpression) => numericExpression |
/ | / | |
modulo(numericExpression, numericExpression) => numericExpression |
% | ||
multiply(numericExpression, numericExpression) => numericExpression |
* | * | |
subtract(numericExpression, numericExpression) => numericExpression |
- | - |
Expressions that use rows returned from a subplan (similar to SQL or SPARQL EXISTS) are not supported in the initial release.
Optic provides node constructor functions that enable you to build tree structures. Node constructor functions can:
The table below summarizes the Optic node constructor functions. For details on each function, see the Optic API reference documentation.
Function | Description |
---|---|
op.jsonArray | Constructs a JSON array with the specified JSON nodes as items. |
op.jsonBoolean | Constructs a JSON boolean node with a specified value. |
op.jsonDocument | Constructs a JSON document with the root content, which must be exactly one JSON object or array node. |
op.jsonNull | Constructs a JSON null node. |
op.jsonNumber | Constructs a JSON number node with a specified value. |
op.jsonObject | Constructs a JSON object with the specified properties. The properties argument is constructed with the prop() function. |
op.jsonString | Constructs a JSON text node with the specified value. |
op.prop | Specifies a key expression and value content for a JSON property of a JSON object. |
op.xmlAttribute | Constructs an XML attribute with a name and atomic value. |
op.xmlComment | Constructs an XML comment with an atomic value. |
op.xmlDocument | Constructs an XML document with a root content. |
op.xmlElement | Constructs an XML element with a name, zero or more attribute nodes, and child content. |
op.xmlPI | Constructs an XML processing instruction with an atomic value. |
op.xmlText | Constructs an XML text node. |
op.xpath | Extracts a sequence of child nodes from a column with node values. |
For example, the following query constructs JSON documents, like the one shown below:
const op = require('/MarkLogic/optic'); const employees = op.fromView('main', 'employees'); employees.select(op.as('Employee', op.jsonDocument( op.jsonObject([op.prop('ID and Name', op.jsonArray([ op.jsonNumber(op.col('EmployeeID')), op.jsonString(op.col('FirstName')), op.jsonString(op.col('LastName')) ])), op.prop('Position', op.jsonString(op.col('Position'))) ]) ))) .result();
This query will produce output that looks like the following:
{ "Employee": { "ID and Name": [ 42, "Debbie", "Goodall" ], "Position": "Senior Widget Researcher" } }
Optic does not have a default/implicit limit for the rows or documents returned. Creating plans that return large result sets, such as tens of thousands of rows, may perform poorly. Should you experience performance problems it is a best practice to page the results using the AccessPlan.prototype.offsetLimit method or a combination of AccessPlan.prototype.offset and AccessPlan.prototype.limit methods.
An Optic Execution Plan expresses a logical dataflow with a sequence of atomic operations. You use the Optic API to build up a plan definition, creating and modifying objects in a pipeline and then executing the plan with the PreparePlan.prototype.result function.
You can use the PreparePlan.prototype.explain function to view or save an execution plan. The execution plan definition consists of operations on a row set. These operations fall into the following categories:
where
, order by
, group
, project with select
, and limit
a row set to yield a modified row set.join
, union
, intersect
, or except
to yield a single row set.When a view is opened as an execution plan, it has a special property that has an object with a property for each column in the view. The name of the property is the column name and the value of the property is a name object. To prevent ambiguity for columns with the same name in different views, the column name for a view column is prefixed with the view name and a separating period.
The execution plan result can be serialized to CSV, line-oriented XML or JSON, depending on the output mime type. For details on how to read an execution plan, see Execution Plan in the SQL Data Modeling Guide.
You use the op.param
function to create a placeholder that can be substituted for any value. You must specify the value of the parameter when executing the plan.
Because the plan engine caches plans, parameterizing a plan executed previously is more efficient than submitting a new plan.
For example, the following query uses a start
and length
parameter to set the offsetLimit and an increment
parameter to increment the value of EmployeeID
.
const op = require('/MarkLogic/optic'); const employees = op.fromView('main', 'employees'); employees.offsetLimit(op.param('start'), op.param('length')) .select(['EmployeeID', op.as('incremented', op.add(op.col('EmployeeID'), op.param('increment')))]) .result(null, {start:1, length:2, increment:1});
You can use the IteratePlan.prototype.export method or op:export function to export a serialized form of an Optic query. This enables the plan to be stored as a file and later imported by the op.import or op:import function or to be used by the /v1/rows
REST call as a payload. You can recreate the source code used to create an exported plan by means of the op.toSource or op:to-source function.
For example, to export an Optic query to a file, do the following:
const op = require('/MarkLogic/optic'); const EmployeePlan = op.fromView('main', 'employees') .select(['EmployeeID', 'FirstName', 'LastName']) .orderBy('EmployeeID') const planObj = EmployeePlan.export(); xdmp.documentInsert("plan.json", planObj)
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; let $plan := op:from-view("main", "employees") => op:select(("EmployeeID", "FirstName", "LastName")) => op:order-by("EmployeeID") => op:export() return xdmp:document-insert("plan.json", xdmp:to-json($plan))
To import an Optic query from a file and output the results, do the following:
const op = require('/MarkLogic/optic'); op.import(cts.doc('plan.json').toObject()) .result();
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:import(fn:doc("plan.json")/node()) => op:result()
To view the source code representation of a plan, do the following:
const op = require('/MarkLogic/optic'); op.toSource(cts.doc('plan.json'))
xquery version "1.0-ml"; import module namespace op="http://marklogic.com/optic" at "/MarkLogic/optic.xqy"; op:to-source(fn:doc("plan.json"))