Skip to content

Commit

Permalink
Add Pimcore Query Language (#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
markus-moser authored May 22, 2024
1 parent 4564bea commit 892148e
Show file tree
Hide file tree
Showing 75 changed files with 4,162 additions and 747 deletions.
11 changes: 0 additions & 11 deletions .github/workflows/codeception.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,6 @@ jobs:
chmod 755 .github/ci/scripts/setup-pimcore-environment-functional-tests.sh
.github/ci/scripts/setup-pimcore-environment-functional-tests.sh
- name: Install SSH Key # this is necessary for Composer to be able to clone source from pimcore/ee-pimcore
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_PRIVATE_KEY_PIMCORE_DEPLOYMENTS_USER }}
known_hosts: '.... we add this in the next step ;-)'

- name: "Add authentication for private pimcore packages"
run: |
composer config --append repositories.private-packagist composer https://repo.pimcore.com/github-actions/
composer config --global --auth http-basic.repo.pimcore.com github-actions ${{ secrets.COMPOSER_PIMCORE_REPO_PACKAGIST_TOKEN }}
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
with:
Expand Down
29 changes: 0 additions & 29 deletions .github/workflows/qodana.yml

This file was deleted.

20 changes: 20 additions & 0 deletions .github/workflows/sonar.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Build
on:
push:
branches:
- 1.x
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarcloud:
name: SonarCloud
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
9 changes: 0 additions & 9 deletions .github/workflows/static-analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,6 @@ jobs:
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
- name: Install SSH Key # this is necessary for Composer to be able to clone source from pimcore/ee-pimcore
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_PRIVATE_KEY_PIMCORE_DEPLOYMENTS_USER }}
known_hosts: '.... we add this in the next step ;-)'
- name: "Add authentication for private pimcore packages"
run: |
composer config --append repositories.private-packagist composer https://repo.pimcore.com/github-actions/
composer config --global --auth http-basic.repo.pimcore.com github-actions ${{ secrets.COMPOSER_PIMCORE_REPO_PACKAGIST_TOKEN }}
- name: "Setup Pimcore environment"
run: |
.github/ci/scripts/setup-pimcore-environment.sh
Expand Down
625 changes: 0 additions & 625 deletions .qodana-profile.xml

This file was deleted.

14 changes: 14 additions & 0 deletions config/services/query-language.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
_defaults:
autowire: true
autoconfigure: false
public: false

Pimcore\Bundle\GenericDataIndexBundle\QueryLanguage\LexerInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\QueryLanguage\Pql\Lexer

Pimcore\Bundle\GenericDataIndexBundle\QueryLanguage\ParserInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\QueryLanguage\Pql\Parser

Pimcore\Bundle\GenericDataIndexBundle\QueryLanguage\ProcessorInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\QueryLanguage\Pql\Processor
22 changes: 21 additions & 1 deletion config/services/search-index-adapter/open-search.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,31 @@ services:
Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\Search\LocateInTreeServiceInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\Search\LocateInTreeService

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\Search\FetchIdsBySearchServiceInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\Search\FetchIdsBySearchService

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\MappingAnalyzerServiceInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\MappingAnalyzerService

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\IndexStatsServiceInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\IndexStatsService

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\IndexMappingServiceInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\IndexMappingService

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\Workspace\QueryServiceInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\Workspace\QueryService
class: Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\Workspace\QueryService

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\QueryLanguage\PqlAdapterInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\QueryLanguage\PqlAdapter

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\QueryLanguage\SubQueriesProcessorInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\QueryLanguage\SubQueriesProcessor

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\QueryLanguage\FieldNameTransformer\:
resource: '../../../src/SearchIndexAdapter/OpenSearch/QueryLanguage/FieldNameTransformer'
tags: ['pimcore.generic_data_index.pql_field_name_transformer']

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\QueryLanguage\FieldNameValidator\:
resource: '../../../src/SearchIndexAdapter/OpenSearch/QueryLanguage/FieldNameValidator'
tags: ['pimcore.generic_data_index.pql_field_name_validator']
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ services:

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\Search\Modifier\Sort\TreeSortHandlers: ~

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\Search\Modifier\QueryLanguage\QueryLanguageHandlers: ~

Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\Search\Modifier\Filter\Workspace\WorkspaceQueryHandler: ~
3 changes: 3 additions & 0 deletions config/services/search/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ services:
Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\LanguageServiceInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\LanguageService

Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\IndexEntityServiceInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\IndexEntityService

Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\IndexUpdateServiceInterface:
class: Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\IndexUpdateService

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ $search->addModifier(new ParentIdFilter(1))
|--------------------------------------------------------------------------------------------|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| [ElementKeySearch](https://github.com/pimcore/generic-data-index-bundle/blob/1.x/src/Model/Search/Modifier/FullTextSearch/ElementKeySearch.php) | Full text search | Search by element key like in the studio UI.<br/><br/>* can be used for wildcard searches - for example "Car*" to find all items starting with "Car". |

### Query Language

| Modifier | Modifier Category | Description |
|----------------------------------------------------------------------------------------------------------------------------------|-------------------|-------------------------------------------------------------------------------------------|
| [PqlFilter](https://github.com/pimcore/generic-data-index-bundle/blob/1.x/src/Model/Search/Modifier/QueryLanguage/PqlFilter.php) | Query Language | Apply a [Pimcore Query Language (PQL)](../09_Pimcore_Query_Language/README.md) condition. |

### Sort Modifiers

| Modifier | Modifier Category | Description |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Use Pimcore Query Language (PQL) as a Developer

## Execute searches based on PQL queries

If you want to use the Pimcore Query Language (PQL) as a developer to search for data in the Pimcore Generic Data Index, you can use one of the following methods:

#### 1. Search Modifier for the Generic Data Index search services

You can use the [PqlFilter](https://github.com/pimcore/generic-data-index-bundle/blob/1.x/src/Model/Search/Modifier/QueryLanguage/PqlFilter.php) search modifier to filter search results based on a PQL query. The `PqlFilter` search modifier can be used with the search services provided by the Generic Data Index bundle. Take a look at the [Search Services](../README.md) documentation for details.

#### 2. Direct use of the PQL processor to get the search query

Use the `Pimcore\Bundle\GenericDataIndexBundle\QueryLanguage\ProcessorInterface` together with the `Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\IndexEntityServiceInterface` to process a PQL query.

```php
// inject both services via Symfony dependency injection
/** @var \Pimcore\Bundle\GenericDataIndexBundle\QueryLanguage\ProcessorInterface $queryLanguageProcessor */
/** @var \Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\IndexEntityServiceInterface $indexEntityService */

$query = $queryLanguageProcessor->process(
'color = "red" or color = "blue"', // The PQL query
$indexEntityService->getByEntityName('Car') // 'Asset', 'Document' or the name of the data object class
);

// $query is now a valid OpenSearch query array which can be used to search in the index
```

## Exception Handling

In both cases, the PQL processor will throw an exception if the PQL query is invalid. The exception message will contain detailed information about the error. Especially when you would like to allow users to enter PQL queries, you should catch the exception and provide a user-friendly error feedback.

##### Example

This example will produce a error message like this:

![PQL Syntax Error](../../img/pql-syntax-error.png)

```php
## Catching the exception
use Pimcore\Bundle\GenericDataIndexBundle\Exception\QueryLanguage\ParsingException;

try {
$pqlQuery = 'series = "E-Type"
and color "red"';

$query = $queryLanguageProcessor->process(
$pqlQuery, // The PQL query
$indexEntityService->getByEntityName('Car') // 'Asset', 'Document' or the name of the data object class
);
} catch (ParsingException $e) {
// Provide user-friendly error feedback
return $twig->render('pql-syntax-error.html.twig', [
'error' => $e->getMessage(),
'syntaxBeforeError' => substr($e->getQuery(), 0, $e->getPosition()),
'syntaxAfterError' => substr($e->getQuery(), $e->getPosition()),
]);
}
```


```twig
{# pql-syntax-error.html.twig #}
<!doctype html>
<html lang="en">
<head>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<style>
.pql-syntax-error {
line-height: 2em;
}
.pql-syntax-error-location {
position: relative;
}
.pql-syntax-error-location span {
position: absolute;
left: -0.5em;
top: 10px;
color: #f44336;
}
</style>
</head>
<body>
<div class="container pt-5">
<div class="alert alert-danger">
<p><strong>{{ error }}</strong></p>
<div class="alert alert-light">
<div class="pql-syntax-error">
{{ syntaxBeforeError|nl2br }}
<span class="pql-syntax-error-location"><span>⇧</span></span>
{{ syntaxAfterError|nl2br }}
</div>
</div>
</div>
</div>
</body>
</html>
```
117 changes: 117 additions & 0 deletions doc/04_Searching_For_Data_In_Index/09_Pimcore_Query_Language/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Pimcore Query Language

Pimcore Query Language (PQL) is a query language that allows you to search for data in the Pimcore Generic Data Index. It is a simple and powerful query language that allows you to search for data using a wide range of search criteria.

## Syntax

Description of the PQL syntax:

```
CONDITION = EXPRESSION | CONDITION ("AND" | "OR") CONDITION
EXPRESSION = "(" CONDITION ")" | COMPARISON | QUERY_STRING_QUERY
COMPARISON = FIELDNAME OPERATOR VALUE | RELATION_COMPARISON
RELATION_COMPARISON = RELATION_FIELD_NAME OPERATOR VALUE
FIELDNAME = IDENTIFIER{.IDENTIFIER}
RELATION_FIELD_NAME = FIELDNAME:ENTITYNAME.FIELDNAME
IDENTIFIER = [a-zA-Z_]\w*
ENTITYNAME = [a-zA-Z_]\w*
OPERATOR = "="|"<"|">"|">="|"<="|"LIKE"
VALUE = INTEGER | FLOAT | "'" STRING "'" | '"' STRING '"'
QUERY_STRING_QUERY = "QUERY('" STRING "')"
```

### Operators

| Operator | Description | Examples |
|----------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|
| `=` | equal | `field = "value"` |
| `<` | smaller than | `field < 100` |
| `<=` | smaller or equal than | `field <= 100` |
| `=>` | bigger or equal than | `field >= 100` |
| `>` | bigger than | `field > 100` |
| `LIKE` | equal with wildcard support<br/><em>* matches zero or more characters</em><br/><em>? matches any single character</em> | `field like "val*"`<br/>`field like "val?e"` |

### AND / OR / Brackets

You can combine multiple conditions using the `AND` and `OR` operators. You can also use brackets to group conditions.

**Examples:**

```
field1 = "value1" AND field2 = "value2"
field1 = "value1" AND (field2 = "value2" OR field3 = "value3")
(field1 = "value1" AND (field2 = "value2" OR field3 = "value3")) or field4 = "value4"
```


### Relation Filters

Supports filtering along relations with following notation:

`<RELATION_FIELD_NAME>:<ENTITY_NAME>.<FIELD_NAME>`

**Examples:**

```
main_image:Asset.type
category:Category.name
manufacturer:Company.country
```

The entity name can be either 'Asset', 'Document' or the name of the data object class.

### Field Names

The field names are named and structured the same way like in the OpenSearch index. Nested field names are supported with a dot ('.') notation.
As described [here](../../05_Extending_Data_Index/06_Extend_Search_Index.md) the fields are separated into three sections (system_fields, standard_fields and custom_fields) and depending on the data type of a attribute the attribute value could be a nested structure with sub-attributes.


**Examples for field names with their full path in the index:**

```
system_fields.id
standard_fields.name
standard_fields.my_relation_field.asset
standard_fields.description.de
```

To simplify the usage of the PQL the field names can be used without the full path in most of the cases. The PQL will automatically search in the index structure and try to detect the correct field. So normally it's enough to use the technical field name like used for example in the data object class or asset metadata attribute.

**Above examples for field names without the full path:**

```
id
name
my_relation_field
description.de
```

Localized fields can be accessed in the form 'field_name.locale' (e.g. description.de).

### Query String Query Filters

The PQL allows passing OpenSearch [query string queries](https://opensearch.org/docs/latest/query-dsl/full-text/query-string/#query-string-syntax) directly to the index. The query string query syntax provides even more flexibility to define the search criteria. Take a look at the [OpenSearch documentation](https://opensearch.org/docs/latest/query-dsl/full-text/query-string/#query-string-syntax) for more details.

**Caution**: The automatic field detection is not supported for query string queries. So you have to use the full path for the field names.

### Example PQL Queries

All examples are based on the `Car` data object class of the [Pimcore Demo](https://pimcore.com/en/try).

| Query | Description |
|---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| `series = "E-Type" AND (color = "green" OR productionYear < 1965)` | All E-Type models which are green or produced before 1965. |
| `manufacturer:Manufacturer.name = "Alfa" and productionYear > 1965` | All Alfa cars produced after 1965. |
| `genericImages:Asset.fullPath like "/Car Images/vw/*"` | All cars with a image linked in the `genericImages` image gallery which is contained in the asset folder `/Car Images/vw`. |
| `color = "red" or color = "blue"` | All red or blue cars using standard PQL syntax. |
| `Query("standard_fields.color:(red or blue)")` | All red or blue cars using simple query string syntax. |


## Limitations

* When searching for related elements the maximum possible results amount of sub queries is 65.000, see also [terms query documentation](https://opensearch.org/docs/latest/query-dsl/term/terms/).
* Filtering for asset metadata fields is only possible if they are defined as predefined asset metadata or via the asset metadata class definitions bundle. Custom asset metadata fields directly defined on single assets are not supported.

## Further Reading

- [Use PQL as a Developer](./03_Use_PQL_as_Developer.md).
Loading

0 comments on commit 892148e

Please sign in to comment.