Skip to main content

Getting Started with Optic

How Does Optic Work?


The Optic API Processing Model article provides a deeper dive into how Optic works.

Optic accesses any MarkLogic data model—table, document, or graph—and represents it as one common model for processing the data: a row sequence, which you can think of as a table. From there, according to your query, Optic manipulates the data as if it were rows and columns with familiar concepts like joins, filters, sorts, and selects. Finally, it produces the results as a row sequence for your app to consume.

Here is a SQL query followed by its Optic equivalent:

SELECT ContributorUserName, UserReputation, UserLocation
FROM Samplestack.Contributors
WHERE UserReputation > 5000
ORDER BY UserReputation
op.fromView('Samplestack', 'Contributors')                           // FROM
  .select(['ContributorUserName', 'UserReputation', 'UserLocation']) // SELECT
  .where('UserReputation'), 5000))                      // WHERE
  .orderBy('UserReputation')                                         // ORDER BY
  .offsetLimit(0,25)                                                 // LIMIT 25 OFFSET 0

When you create an Optic query or update, you create a pipeline:

Data Accessor Function-->Operator Function(s)--> Executor Function (JavaScript & XQuery)

  • With Data Accessor (or Data Access) Functions like fromView(), analogous to SQL’s FROM statement, you tell Optic where to look for data. You use only one data accessor function per query or update (except in ones that by nature require two data sources: joins, unions, intersections, and excepts).

  • With the Operator (or ModifyPlan) Function select(), analogous to SQL’s SELECT statement, you select the columns you need from the data source.

  • With operator functions like orderBy(), where(), and offsetLimit(), analogous to SQL’s ORDER BY, WHERE, and LIMIT + OFFSET statements, you tell Optic how you want that data manipulated. You can have many operator functions in a query or update.

  • For JavaScript and XQuery, with an Executor (or IteratePlan) Function like result(), you execute the pipeline and receive its results. You use only one executor function per query or update.

When the executor function executes the query or update, Optic optimizes the pipeline to minimize memory usage and maximize performance.

So, to satisfy the example query, upon hitting result(), Optic uses fromView() to project the required data from the correct documents into a row sequence. It then uses the most time- and memory-efficient way to manipulate the row sequence with select() reducing the number of columns, orderBy() determining the order of rows, where() reducing the rows to those containing specified values in specified columns, and offsetLimit() restricting the number of resulting rows. Finally, it uses result() to produce the processed rows. These rows are ready to be consumed by your application.

More details on how Optic works will unfold as you see how we have built the following queries and updates to explore and change some sample data.


If your query or update requires calling built-in MarkLogic functions against every row in a column, turn those functions into Optic Expression Functions (formerly called Value Processing Functions) by using the op. prefix.