Using the CQL Query Module
Steps
The Confluence Query Language (CQL) is used in Confluence to build complex document queries.
For a description of how this module was designed, please see Design.
Basic Usage
To query XWiki using CQL, you can create a query and execute it the familiar way, using "cql" as the query type. The CQL query is translated to Sorl's standard syntax and executed using Solr. The results returned by the execute function are the same as for a Solr query: results are found in the results property of the first and only returned row and which type is org.apache.solr.common.SolrDocumentList. There's no wrapping or abstraction of the Solr result part in the CQL Query Module.
In Velocity:
{{velocity}}
#set ($query = $services.query.createQuery("creator = currentUser() and lastmodified > startOfWeek() order by title", "cql").setLimit(5))
#set ($results = $query.execute()[0].results)
#foreach ($result in $results)
* $result.id
#end
{{/velocity}}In Groovy:
{{groovy}}
def query = services.query.createQuery("creator = currentUser() and lastmodified > startOfWeek() order by title", "cql").setLimit(5)
def results = query.execute()[0].results
for (def result : results) {
println "* " + result.id
}
{{/groovy}}In Java:
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.xwiki.component.annotation.Component;
import org.xwiki.query.QueryException;
import org.xwiki.query.QueryManager;
@Component (roles = DefaultScratch.class)
@Singleton
class DefaultScratch
{
@Inject
private QueryManager queryManager;
public List<Object> example() throws QueryException
{
return queryManager.createQuery("title = 'Main'", "cql").setLimit(1).execute();
}
}Sorting
It is possible to order results by specifying an order by clause in the CQL statement. The order by clause will need to follow the name of the fields in Confluence and will be converted to the corresponding XWiki Solr fields. XWiki Solr fields cannot be used directly in a CQL expression.
It is also possible to do so by binding a sort parameter. The sort parameter will then override whatever order by clause might be present in the CQL query. In the sort parameter, the XWiki Solr fields need to be used directly, Confluence fields cannot be used there:
#set ($query = $services.query.createQuery("...", "cql").sort("creator_display asc"))Supported syntax
See the documentation of the supported parts of the CQL Syntax and how it is translated.
Debugging a query that doesn't work as expected
A query that doesn't work as expected, apart from being caused by unexpected results and unexpectedly present / absent documents in the wiki, will most likely be caused by:
- a parse error
- a conversion error
- an incorrect translation to Solr
Parse and Conversion errors
A parse error would happen if something is wrong with the syntax of the query. A conversion error would happen if something is wrong with how it's "understood" or if something else prevent the query to be converted to Solr, for instance a missing feature, after the parsing is done.
They will both raise exceptions so it should be easy to get to the root cause quite rapidly. Care has been taken to make error messages as understandable as possible, with precise pointing of the error:
Incorrect translation to Solr
It can happen that the translation to Solr is incorrect due to a bug in the translator, or in a component that extends the translation coming from elsewhere (as described below).
In this case, it can be useful to enable DEBUG log level for the org.contrib.cql package. The queries and their Solr translations are printed each time a query is executed.
Spot lines like:
2024-05-31 11:50:42,651 [qtp1991294891-812 - http://localhost:8080/xwiki/bin/view/Main/WebHome] DEBUG o.x.c.c.q.i.CQLQueryExecutor - CQL Statement [(label = "hello" OR label = "world") AND type = "page"] converted to Solr query [((property.XWiki.TagClass.tags:hello) OR (property.XWiki.TagClass.tags:world)) AND (type:DOCUMENT AND -class:Blog.BlogPostClass)], sort parameter [] for execution
Translating CQL to Solr queries without querying
You may want to convert CQL statements to Solr statements, for instance if you want to provide a bridge between some Confluence feature to an equivalent Solr-based feature.
This is quite straight forward. You need to parse the CQL statement using AQLParser, and call the getSolrStatement and getSolrSort methods of the org.xwiki.contrib.cql.query.converters.CQLToSolrQueryConverter component.
import java.io.IOException;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.xwiki.component.annotation.Component;
import org.xwiki.contrib.cql.aqlparser.AQLParser;
import org.xwiki.contrib.cql.aqlparser.ast.AQLStatement;
import org.xwiki.contrib.cql.aqlparser.exceptions.ParserException;
import org.xwiki.contrib.cql.query.converters.CQLToSolrQueryConverter;
@Component (roles = DefaultScratch.class)
@Singleton
class DefaultScratch
{
@Inject
private CQLToSolrQueryConverter cqlConverter;
public void example() throws ParserException, IOException
{
AQLStatement cql = AQLParser.parse( "title = 'Main'");
String solr = cqlConverter.getSolrStatement(cql);
String sort = cqlConverter.getSolrSortParameter(cql);
// do something with solr and sort
}
}Advanced topics
This section goes beyond basic usage and is intended to developers seeking to improving or altering the behavior of the CQL query to Solr translator.
Although improving the CQL to Solr translation should ideally be done in the cql module itself, it's not always desirable or possible. In which case, the CQL Query module offers way to extend the translation at different levels.
Common stuff
In what follows:
- throw a ConversionException when there is an obvious error somewhere, or when you called code that throws
- return null when you didn't find something or you don't know how to convert something
For instance, if you couldn't resolve document with id 42 or space DEMO, return null. But if the query is wrong (for instance lastmodified > "hello bad world"), you can throw a ConversionExpression. However, it would be recommended to null in this case too, so the default converter or another converter can handle the expressions you wouldn't expect but was legit after all, and throw if appropriate.
Providing Confluence IDs to the translator
Some CQL expressions like `id = 42` require knowing which XWiki document corresponds to the given Confluence ID (here 42) to be translated to Solr.
When migrating content from Confluence to XWiki using Confluence XML, it is possible to enable the Store Confluence details parameter. When doing so, Confluence.Code.ConfluencePageClass objects will be added to each document, with some confluence metadata like their IDs.
The translator will try to probe the wiki and find such Confluence.Code.ConfluencePageClass objects to convert such CQL expressions.
However, the Store Confluence details parameter isn't enabled by default and the Confluence.Code.ConfluencePageClass object corresponding to a particular Confluence ID might not be there.
It is possible to help the translator find documents corresponding to a given ID by providing a component implementing the org.xwiki.contrib.cql.query.converters.ConfluenceIdResolver interface. Please give the component a name using the @Named annotation.
For convenience, here is a copy of this interface, but please actually refer to the newest version of the linked file.
package org.xwiki.contrib.cql.query.converters;
import org.xwiki.component.annotation.Role;
import org.xwiki.contrib.cql.aqlparser.ast.AbstractAQLRightHandValue;
import org.xwiki.model.reference.EntityReference;
/**
* Find a document from its Confluence ID.
* @version $Id$
* @since 0.0.1
*/
@Role
public interface ConfluenceIdResolver
{
/**
* @return the document in XWiki, or null if the document is not found.
* @param node the CQL node containing this id, or null if not applicable
* @param id the Confluence ID of the document
* @throws ConversionException when something bad happens
*/
EntityReference getDocumentById(AbstractAQLRightHandValue node, long id) throws ConversionException;
}Here's the provided implementation, which uses ConfluencePageClass objects: https://github.com/xwiki-contrib/cql/blob/master/query/src/main/java/org/xwiki/contrib/cql/query/converters/internal/ConfluencePageClassConfluenceIdResolver.java
The translator will try to resolve IDs using all the registered components, until one returns a non-null entity reference.
Providing the migrated location of Confluence spaces to the translator
Some CQL expressions like `space = DEMO` require knowing which XWiki document corresponds to the given Confluence ID (here 42) to be translated to Solr.
By default, the translator will blindly convert a Confluence space key to the entity reference of the root space of the same name (i.e the space key DEMO is translated to xwiki:DEMO). This is a fallback mechanism which works most of the time.
However, there are some situations where this doesn't work. For instance, for Confluence spaces that were migrated with a non empty "Root space" parameter of Extension.Confluence.XML]. It is possible to help the translator find spaces corresponding to a given space key by providing a component implementing the [[org.xwiki.contrib.cql.query.converters.ConfluenceIdResolver interface. Please give the component a name using the @Named annotation.
For convenience, here is a copy of this interface, but please actually refer to the newest version of the linked file.
package org.xwiki.contrib.cql.query.converters;
import org.xwiki.component.annotation.Role;
import org.xwiki.contrib.cql.aqlparser.ast.AbstractAQLRightHandValue;
import org.xwiki.model.reference.EntityReference;
/**
* Find a space from its Confluence key.
* @version $Id$
* @since 0.0.1
*/
@Role
public interface ConfluenceSpaceResolver
{
/**
* @return the space in XWiki, or null if the document is not found.
* @param node the CQL node containing this id, or null if not applicable
* @param spaceKey the Confluence ID of the document
* @throws ConversionException when something bad happens
*/
EntityReference getSpaceByKey(AbstractAQLRightHandValue node, String spaceKey) throws ConversionException;
/**
* @return the space reference (of type EntityType.SPACE) to the root of the migrated Confluence space in which the
* current document is.
* @param node the CQL node requesting the current space (likely a 'currentSpace()' call)
* @throws ConversionException when something bad happens
*/
EntityReference getCurrentConfluenceSpace(AbstractAQLRightHandValue node) throws ConversionException;
}The translator will try to resolve space keys using all the registered components, until one returns a non-null entity reference.
Adding or modifying the translation for a particular field
The provided translation is not complete, and CQL is also extensible. It is possible to add or modify the existing translation for a particular field by implementing the org.xwiki.contrib.cql.query.converters.CQLToSolrSortFieldConverter interface, copied here for your convenience but please refer to the latest version of this file:
package org.xwiki.contrib.cql.query.converters;
import org.xwiki.component.annotation.Role;
import org.xwiki.contrib.cql.aqlparser.ast.AQLAtomicClause;
import org.xwiki.stability.Unstable;
/**
* CQL to Solr Atom Converter.
* @since 0.0.1
* @version $Id$
*/
@Role
@Unstable
public interface CQLToSolrAtomConverter
{
/**
* @return the given CQL atom converted to a Solr atom, as string
* @param atom the atom to convert
* @throws ConversionException if something wrong happens
*/
String convertToSolr(AQLAtomicClause atom) throws ConversionException;
/**
* @return the name(s) of the fields handled by this converter, as a String equal to the name or a Pattern
* matching the name. If null, the name of the component will be used.
* @since 0.2.0
*/
default Object getHandledFields()
{
return null;
}
}Your component should have a name (hint) specific to the name of the field(s) it handles. If the field has a simple name, the hint is sufficient. If you must handle fields that don't have a fixed name (there's some dynamic part in the field), Implement the getHandledFields(). It should return a String equal to the name of the field, or a Pattern that matches the field. An example of this is https://github.com/xwikisas/xwiki-pro-macros/blob/04c586092e308cb86d32441febeb54adbece535c/xwiki-pro-macros-confluence-bridges/xwiki-pro-macros-confluence-bridges-api/src/main/java/com/xwiki/macros/confluence/internal/cql/MetadataFieldCQLToSolrConverter.java#L44-L58
Your component should take the Abstract Syntax Tree node corresponding to an atomic CQL expression, and return the corresponding Solr code.
As this is not a trivial task and it would be undesirable to reimplement all the logic that is already implemented in the CQL Query module, it is advised to extend org.xwiki.contrib.cql.query.converters.DefaultCQLToSolrAtomConverter instead of writing your component from scratch.
DefaultCQLToSolrAtomConverter implements convertToSolr(AQLAtomicClause atom, TYPE right) throws ConversionException methods for all supported CQL types.
You can then:
- override List<String> getSolrFields(AQLAtomicClause atom) throws ConversionException to provide the Solr field(s) in which to search
- override the specific method of the types you want to support and you only need to return the converted value
For instance:
- override String convertToSolr(AQLAtomicClause atom, AQLNumberLiteral expression) throws ConversionException to implement a numerical field
- override String convertToSolr(AQLAtomicClause atom, AQLDateLiteral expression) throws ConversionException to implement a date field
- override String convertToSolr(AQLAtomicClause atom, AQLBooleanLiteral expression) throws ConversionException to implement a boolean field
- override String convertToSolr(AQLAtomicClause atom, AQLStringLiteral expression) throws ConversionException to implement a string field
If you don't specifically want to have to deal with specific types of values, you can alternatively override String convertToSolr(AQLAtomicClause atom, AbstractAQLAtomicValue right) throws ConversionException and call super, like this:
//...
@Override
protected String convertToSolr(AQLAtomicClause atom, AbstractAQLAtomicValue right) throws ConversionException
{
String value = super.convertToSolr(atom, right);
// do something to value, which is the default escaped Solr value
return value; // beware, this should be solr-escaped
}
@Override
protected List<String> getSolrFields(AQLAtomicClause atom) throws ConversionException
{
return List.of("title_sort"); // return solr fields corresponding to this atomic expression
}
//...
}There is an example of this at https://github.com/xwiki-contrib/cql/blob/master/query/src/main/java/org/xwiki/contrib/cql/query/converters/internal/AncestorCQLToSolrAtomConverter.java, which extends https://github.com/xwiki-contrib/cql/blob/master/query/src/main/java/org/xwiki/contrib/cql/query/converters/internal/AbstractIdCQLToSolrAtomConverter.java.
More
To find more about the current topic, you can search or use the table below and filter the columns to narrow your choices.