Wiki source code of Using the CQL Query Module
Last modified by superadmin on 2026/10/14 18:44
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 1 | {{warning}} | ||
| 2 | This document should probably be split into smaller, more focused documents. | ||
| 3 | {{/warning}} | ||
| 4 | |||
| 5 | The Confluence Query Language (CQL) is used in Confluence to build complex document queries. | ||
| 6 | |||
| 7 | For a description of how this module was designed, please see [[Design>>doc:.design.WebHome]]. | ||
| 8 | |||
| 9 | {{warning}} | ||
| 10 | This module is still in a very early stage in its life. Expect changes, missing features and surprising behavior. | ||
| 11 | {{/warning}} | ||
| 12 | |||
| 13 | {{info}} | ||
| 14 | This module is intended at developers of code providing compatibility and interoperability with Confluence. Anybody else is urged to use native features of XWiki. End users are invited to use the [[Solr Search Application>>doc:extensions:Extension.Solr Search Application]] and developers the [[extension:Extension.Solr Search Query API]]. | ||
| 15 | {{/info}} | ||
| 16 | |||
| 17 | == Basic Usage == | ||
| 18 | |||
| 19 | 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. | ||
| 20 | |||
| 21 | In Velocity: | ||
| 22 | |||
| 23 | {{code language="velocity"}} | ||
| 24 | {{velocity}} | ||
| 25 | #set ($query = $services.query.createQuery("creator = currentUser() and lastmodified > startOfWeek() order by title", "cql").setLimit(5)) | ||
| 26 | #set ($results = $query.execute()[0].results) | ||
| 27 | #foreach ($result in $results) | ||
| 28 | * $result.id | ||
| 29 | #end | ||
| 30 | {{/velocity}} | ||
| 31 | {{/code}} | ||
| 32 | |||
| 33 | In Groovy: | ||
| 34 | |||
| 35 | {{code language="groovy"}} | ||
| 36 | {{groovy}} | ||
| 37 | def query = services.query.createQuery("creator = currentUser() and lastmodified > startOfWeek() order by title", "cql").setLimit(5) | ||
| 38 | def results = query.execute()[0].results | ||
| 39 | for (def result : results) { | ||
| 40 | println "* " + result.id | ||
| 41 | } | ||
| 42 | {{/groovy}} | ||
| 43 | {{/code}} | ||
| 44 | |||
| 45 | In Java: | ||
| 46 | |||
| 47 | {{code language="java"}} | ||
| 48 | import java.util.List; | ||
| 49 | |||
| 50 | import javax.inject.Inject; | ||
| 51 | import javax.inject.Singleton; | ||
| 52 | |||
| 53 | import org.xwiki.component.annotation.Component; | ||
| 54 | import org.xwiki.query.QueryException; | ||
| 55 | import org.xwiki.query.QueryManager; | ||
| 56 | |||
| 57 | @Component (roles = DefaultScratch.class) | ||
| 58 | @Singleton | ||
| 59 | class DefaultScratch | ||
| 60 | { | ||
| 61 | @Inject | ||
| 62 | private QueryManager queryManager; | ||
| 63 | |||
| 64 | public List<Object> example() throws QueryException | ||
| 65 | { | ||
| 66 | return queryManager.createQuery("title = 'Main'", "cql").setLimit(1).execute(); | ||
| 67 | } | ||
| 68 | } | ||
| 69 | {{/code}} | ||
| 70 | |||
| 71 | === Sorting === | ||
| 72 | |||
| 73 | 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. | ||
| 74 | 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: | ||
| 75 | |||
| 76 | {{code language="velocity"}} | ||
| 77 | #set ($query = $services.query.createQuery("...", "cql").sort("creator_display asc")) | ||
| 78 | {{/code}} | ||
| 79 | |||
| 80 | === Supported syntax === | ||
| 81 | |||
| 82 | See the [[documentation of the supported parts of the CQL Syntax and how it is translated>>xwiki:documentation.extensions.dev.cql.syntax.WebHome]]. | ||
| 83 | |||
| 84 | === Debugging a query that doesn't work as expected === | ||
| 85 | |||
| 86 | 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: | ||
| 87 | |||
| 88 | * a parse error | ||
| 89 | * a conversion error | ||
| 90 | * an incorrect translation to Solr | ||
| 91 | |||
| 92 | ==== Parse and Conversion errors ==== | ||
| 93 | |||
| 94 | 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. | ||
| 95 | |||
| 96 | 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: | ||
| 97 | |||
| 98 | {{image reference="parseerror.png"/}} | ||
| 99 | |||
| 100 | {{image reference="parseerrordetailed.png"/}} | ||
| 101 | |||
| 102 | ==== Incorrect translation to Solr ==== | ||
| 103 | |||
| 104 | 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). | ||
| 105 | |||
| 106 | 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. | ||
| 107 | |||
| 108 | Spot lines like: | ||
| 109 | |||
| 110 | {{{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 | ||
| 111 | }}} | ||
| 112 | |||
| 113 | === Translating CQL to Solr queries without querying === | ||
| 114 | |||
| 115 | 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. | ||
| 116 | |||
| 117 | This is quite straight forward. You need to parse the CQL statement using [[AQLParser>>doc:documentation.extensions.dev.cql.aql-parser.WebHome]], and call the ##getSolrStatement## and ##getSolrSort## methods of the ##org.xwiki.contrib.cql.query.converters.CQLToSolrQueryConverter## component. | ||
| 118 | |||
| 119 | {{code language="java"}} | ||
| 120 | import java.io.IOException; | ||
| 121 | import java.util.List; | ||
| 122 | |||
| 123 | import javax.inject.Inject; | ||
| 124 | import javax.inject.Singleton; | ||
| 125 | |||
| 126 | import org.xwiki.component.annotation.Component; | ||
| 127 | import org.xwiki.contrib.cql.aqlparser.AQLParser; | ||
| 128 | import org.xwiki.contrib.cql.aqlparser.ast.AQLStatement; | ||
| 129 | import org.xwiki.contrib.cql.aqlparser.exceptions.ParserException; | ||
| 130 | import org.xwiki.contrib.cql.query.converters.CQLToSolrQueryConverter; | ||
| 131 | |||
| 132 | @Component (roles = DefaultScratch.class) | ||
| 133 | @Singleton | ||
| 134 | class DefaultScratch | ||
| 135 | { | ||
| 136 | @Inject | ||
| 137 | private CQLToSolrQueryConverter cqlConverter; | ||
| 138 | |||
| 139 | public void example() throws ParserException, IOException | ||
| 140 | { | ||
| 141 | AQLStatement cql = AQLParser.parse( "title = 'Main'"); | ||
| 142 | String solr = cqlConverter.getSolrStatement(cql); | ||
| 143 | String sort = cqlConverter.getSolrSortParameter(cql); | ||
| 144 | // do something with solr and sort | ||
| 145 | } | ||
| 146 | } | ||
| 147 | {{/code}} | ||
| 148 | |||
| 149 | == Advanced topics == | ||
| 150 | |||
| 151 | 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. | ||
| 152 | |||
| 153 | 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. | ||
| 154 | |||
| 155 | === Common stuff === | ||
| 156 | |||
| 157 | In what follows: | ||
| 158 | |||
| 159 | * throw a ##ConversionException## when there is an obvious error somewhere, or when you called code that throws | ||
| 160 | * return null when you didn't find something or you don't know how to convert something | ||
| 161 | |||
| 162 | 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. | ||
| 163 | |||
| 164 | === Providing Confluence IDs to the translator === | ||
| 165 | |||
| 166 | Some CQL expressions like `id = 42` require knowing which XWiki document corresponds to the given Confluence ID (here 42) to be translated to Solr. | ||
| 167 | |||
| 168 | When migrating content from Confluence to XWiki using [[Confluence XML>>doc:documentation.extensions.user.confluence.WebHome]], 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. | ||
| 169 | |||
| 170 | The translator will try to probe the wiki and find such ##Confluence.Code.ConfluencePageClass## objects to convert such CQL expressions. | ||
| 171 | |||
| 172 | 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. | ||
| 173 | |||
| 174 | 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##>>https://github.com/xwiki-contrib/cql/blob/master/query/src/main/java/org/xwiki/contrib/cql/query/converters/ConfluenceIdResolver.java]] interface. Please give the component a name using the ##@Named## annotation. | ||
| 175 | |||
| 176 | For convenience, here is a copy of this interface, but please actually refer to the newest version of the linked file. | ||
| 177 | |||
| 178 | {{code language="java"}} | ||
| 179 | package org.xwiki.contrib.cql.query.converters; | ||
| 180 | |||
| 181 | import org.xwiki.component.annotation.Role; | ||
| 182 | import org.xwiki.contrib.cql.aqlparser.ast.AbstractAQLRightHandValue; | ||
| 183 | import org.xwiki.model.reference.EntityReference; | ||
| 184 | |||
| 185 | /** | ||
| 186 | * Find a document from its Confluence ID. | ||
| 187 | * @version $Id$ | ||
| 188 | * @since 0.0.1 | ||
| 189 | */ | ||
| 190 | @Role | ||
| 191 | public interface ConfluenceIdResolver | ||
| 192 | { | ||
| 193 | /** | ||
| 194 | * @return the document in XWiki, or null if the document is not found. | ||
| 195 | * @param node the CQL node containing this id, or null if not applicable | ||
| 196 | * @param id the Confluence ID of the document | ||
| 197 | * @throws ConversionException when something bad happens | ||
| 198 | */ | ||
| 199 | EntityReference getDocumentById(AbstractAQLRightHandValue node, long id) throws ConversionException; | ||
| 200 | } | ||
| 201 | {{/code}} | ||
| 202 | |||
| 203 | 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 | ||
| 204 | |||
| 205 | The translator will try to resolve IDs using all the registered components, until one returns a non-null entity reference. | ||
| 206 | |||
| 207 | === Providing the migrated location of Confluence spaces to the translator === | ||
| 208 | |||
| 209 | Some CQL expressions like `space = DEMO` require knowing which XWiki document corresponds to the given Confluence ID (here 42) to be translated to Solr. | ||
| 210 | |||
| 211 | 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. | ||
| 212 | |||
| 213 | 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##>>https://github.com/xwiki-contrib/cql/blob/master/query/src/main/java/org/xwiki/contrib/cql/query/converters/ConfluenceSpaceResolver.java]] interface. Please give the component a name using the ##@Named## annotation. | ||
| 214 | |||
| 215 | For convenience, here is a copy of this interface, but please actually refer to the newest version of the linked file. | ||
| 216 | |||
| 217 | {{code language="java"}} | ||
| 218 | package org.xwiki.contrib.cql.query.converters; | ||
| 219 | |||
| 220 | import org.xwiki.component.annotation.Role; | ||
| 221 | import org.xwiki.contrib.cql.aqlparser.ast.AbstractAQLRightHandValue; | ||
| 222 | import org.xwiki.model.reference.EntityReference; | ||
| 223 | |||
| 224 | /** | ||
| 225 | * Find a space from its Confluence key. | ||
| 226 | * @version $Id$ | ||
| 227 | * @since 0.0.1 | ||
| 228 | */ | ||
| 229 | @Role | ||
| 230 | public interface ConfluenceSpaceResolver | ||
| 231 | { | ||
| 232 | /** | ||
| 233 | * @return the space in XWiki, or null if the document is not found. | ||
| 234 | * @param node the CQL node containing this id, or null if not applicable | ||
| 235 | * @param spaceKey the Confluence ID of the document | ||
| 236 | * @throws ConversionException when something bad happens | ||
| 237 | */ | ||
| 238 | EntityReference getSpaceByKey(AbstractAQLRightHandValue node, String spaceKey) throws ConversionException; | ||
| 239 | |||
| 240 | /** | ||
| 241 | * @return the space reference (of type EntityType.SPACE) to the root of the migrated Confluence space in which the | ||
| 242 | * current document is. | ||
| 243 | * @param node the CQL node requesting the current space (likely a 'currentSpace()' call) | ||
| 244 | * @throws ConversionException when something bad happens | ||
| 245 | */ | ||
| 246 | EntityReference getCurrentConfluenceSpace(AbstractAQLRightHandValue node) throws ConversionException; | ||
| 247 | } | ||
| 248 | {{/code}} | ||
| 249 | |||
| 250 | The translator will try to resolve space keys using all the registered components, until one returns a non-null entity reference. | ||
| 251 | |||
| 252 | === Adding or modifying the translation for a particular field === | ||
| 253 | |||
| 254 | 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##>>https://github.com/xwiki-contrib/cql/blob/master/query/src/main/java/org/xwiki/contrib/cql/query/converters/CQLToSolrSortFieldConverter.java]] interface, copied here for your convenience but please refer to the latest version of this file: | ||
| 255 | |||
| 256 | {{code language="java"}} | ||
| 257 | package org.xwiki.contrib.cql.query.converters; | ||
| 258 | |||
| 259 | import org.xwiki.component.annotation.Role; | ||
| 260 | import org.xwiki.contrib.cql.aqlparser.ast.AQLAtomicClause; | ||
| 261 | import org.xwiki.stability.Unstable; | ||
| 262 | |||
| 263 | /** | ||
| 264 | * CQL to Solr Atom Converter. | ||
| 265 | * @since 0.0.1 | ||
| 266 | * @version $Id$ | ||
| 267 | */ | ||
| 268 | @Role | ||
| 269 | @Unstable | ||
| 270 | public interface CQLToSolrAtomConverter | ||
| 271 | { | ||
| 272 | /** | ||
| 273 | * @return the given CQL atom converted to a Solr atom, as string | ||
| 274 | * @param atom the atom to convert | ||
| 275 | * @throws ConversionException if something wrong happens | ||
| 276 | */ | ||
| 277 | String convertToSolr(AQLAtomicClause atom) throws ConversionException; | ||
| 278 | |||
| 279 | /** | ||
| 280 | * @return the name(s) of the fields handled by this converter, as a String equal to the name or a Pattern | ||
| 281 | * matching the name. If null, the name of the component will be used. | ||
| 282 | * @since 0.2.0 | ||
| 283 | */ | ||
| 284 | default Object getHandledFields() | ||
| 285 | { | ||
| 286 | return null; | ||
| 287 | } | ||
| 288 | } | ||
| 289 | {{/code}} | ||
| 290 | |||
| 291 | 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 | ||
| 292 | |||
| 293 | Your component should take the Abstract Syntax Tree node corresponding to an atomic CQL expression, and return the corresponding Solr code. | ||
| 294 | |||
| 295 | 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##>>https://github.com/xwiki-contrib/cql/blob/master/query/src/main/java/org/xwiki/contrib/cql/query/converters/internal/AbstractIdCQLToSolrAtomConverter.java]] instead of writing your component from scratch. | ||
| 296 | |||
| 297 | ##DefaultCQLToSolrAtomConverter## implements ##convertToSolr(AQLAtomicClause atom, TYPE right) throws ConversionException## methods for all supported CQL types. | ||
| 298 | |||
| 299 | You can then: | ||
| 300 | |||
| 301 | * override ##List<String> getSolrFields(AQLAtomicClause atom) throws ConversionException## to provide the Solr field(s) in which to search | ||
| 302 | * override the specific method of the types you want to support and you only need to return the converted value | ||
| 303 | |||
| 304 | For instance: | ||
| 305 | |||
| 306 | * override ##String convertToSolr(AQLAtomicClause atom, AQLNumberLiteral expression) throws ConversionException ## to implement a numerical field | ||
| 307 | * override ##String convertToSolr(AQLAtomicClause atom, AQLDateLiteral expression) throws ConversionException ## to implement a date field | ||
| 308 | * override ##String convertToSolr(AQLAtomicClause atom, AQLBooleanLiteral expression) throws ConversionException## to implement a boolean field | ||
| 309 | * override ##String convertToSolr(AQLAtomicClause atom, AQLStringLiteral expression) throws ConversionException## to implement a string field | ||
| 310 | |||
| 311 | 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: | ||
| 312 | |||
| 313 | {{code language="java"}} | ||
| 314 | //... | ||
| 315 | @Override | ||
| 316 | protected String convertToSolr(AQLAtomicClause atom, AbstractAQLAtomicValue right) throws ConversionException | ||
| 317 | { | ||
| 318 | String value = super.convertToSolr(atom, right); | ||
| 319 | // do something to value, which is the default escaped Solr value | ||
| 320 | return value; // beware, this should be solr-escaped | ||
| 321 | } | ||
| 322 | |||
| 323 | @Override | ||
| 324 | protected List<String> getSolrFields(AQLAtomicClause atom) throws ConversionException | ||
| 325 | { | ||
| 326 | return List.of("title_sort"); // return solr fields corresponding to this atomic expression | ||
| 327 | } | ||
| 328 | |||
| 329 | //... | ||
| 330 | } | ||
| 331 | {{/code}} | ||
| 332 | |||
| 333 | 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. |