Skip to content

Content search

You can search for content with the PHP API in two ways.

To do this, you can use the SearchService or Repository filtering.

SearchService

SearchService enables you to perform search queries using the PHP API.

The service should be injected into the constructor of your command or controller.

SearchService in the Back Office

SearchService is also used in the Back Office of Ibexa Platform, in components such as Universal Discovery Widget or Sub-items List.

To search through content you need to create a LocationQuery and provide your search criteria as a series of Criterion objects.

For example, to search for all content of a selected Content Type, use one Criterion, Criterion\ContentTypeIdentifier (line 14).

The following command takes the Content Type identifier as an argument and lists all results:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//...
use eZ\Publish\API\Repository\SearchService;
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;

class FindContentCommand extends Command
{
    // ...
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $contentTypeId = $input->getArgument('contentTypeId');

        $query = new LocationQuery();
        $query->filter = new Criterion\ContentTypeIdentifier($contentTypeId);

        $result = $this->searchService->findContentInfo($query);
        $output->writeln('Found ' . $result->totalCount . ' items');
        foreach ($result->searchHits as $searchHit) {
            $output->writeln($searchHit->valueObject->name);
        }
    }
}

SearchService::findContentInfo (line 16) retrieves ContentInfo objects of the found Content items. You can also use SearchService::findContent to get full Content objects, together with their Field information.

To query for a single result, for example by providing a Content ID, use the SearchService::findSingle method:

1
2
3
$criterion = new Criterion\ContentId($contentId);
$result = $this->searchService->findSingle($criterion);
$output->writeln($result->getName());

Tip

For full list and details of available Search Criteria, see Search Criteria reference.

Search result limit

By default search returns up to 25 results. You can change it by setting a different limit to the query:

1
$query->limit = 100;

Search with query and filter

You can use two properties of the Query object to search for content: query and filter.

In contrast to filter, query has an effect of search scoring (relevancy). It affects default sorting if no Sort Clause is used. As such, query is recommended when the search is based on user input.

The difference between query and filter is only relevant when using Solr search engine. With the Legacy search engine both properties will give identical results.

Repository filtering

You can use the ContentService::find(Filter) method to find Content items or LocationService::find(Filter) to find Locations using a defined Filter.

ContentService::find returns an iterable ContentList while LocationService::find returns an iterable LocationList.

Filtering differs from search. It does not use the SearchService and is not based on indexed data.

Filter enables you to configure a query using chained methods to select criteria, sorting, limit and offset.

For example, the following command lists all Content items under the specified parent Location and sorts them by name in descending order:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ...
use eZ\Publish\API\Repository\ContentService;
use eZ\Publish\API\Repository\Values\Filter\Filter;
use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;

class FilterCommand extends Command
{
    // ...
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $parentLocationId = (int)$input->getArgument('parent-location-id');

        $filter = new Filter();
        $filter
            ->withCriterion(new Criterion\ParentLocationId($parentLocationId))
            ->withSortClause(new SortClause\ContentName(Query::SORT_DESC));

        $result = $this->contentService->find($filter, []);

        $output->writeln('Found ' . $result->getTotalCount() . ' items');

        foreach ($result as $content) {
            $output->writeln($content->getName());
        }

        return self::SUCCESS;
    }
}

The same Filter can be applied to find Locations instead of Content items, for example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
use eZ\Publish\API\Repository\LocationService;
use eZ\Publish\API\Repository\Values\Filter\Filter;
use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;

class FilterCommand extends Command
{
    // ...
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $parentLocationId = (int)$input->getArgument('parent-location-id');
        $filter = new Filter();
        $filter
            ->withCriterion(new Criterion\ParentLocationId($parentLocationId))
            ->withSortClause(new SortClause\ContentName(Query::SORT_DESC));

        $result = $this->locationService->find($filter, []);

        $output->writeln('Found ' . $result->totalCount . ' items');

        foreach ($result as $location) {
            $output->writeln($location->getContent()->getName());
        }

        return self::SUCCESS;
    }
}

Notice that the total number of items is retrieved differently for ContentList and LocationList.

Caution

The total count is the total number of matched items, regardless of pagination settings.

Repository filtering is SiteAccess-aware

Repository filtering is SiteAccess-aware, which means you can skip the second argument of the find methods. In that case languages from a current context are injected and added as a LanguageCode Criterion filter.

You can use the following methods of the Filter:

  • withCriterion - add the first Criterion to the Filter
  • andWithCriterion - add another Criterion to the Filter using a LogicalAnd operation. If this is the first Criterion, this method works like withCriterion
  • orWithCriterion - add a Criterion using a LogicalOr operation. If this is the first Criterion, this method works like withCriterion
  • withSortClause - add a Sort Clause to the Filter
  • sliceBy - set limit and offset for pagination
  • reset - remove all Criteria, Sort Clauses, and pagination settings

The following example filters for Folder Content items under the parent Location 2, sorts them by publication date and returns 10 results, starting from the third one:

1
2
3
4
5
6
$filter = new Filter();
$filter
    ->withCriterion(new Criterion\ContentTypeIdentifier('folder'))
    ->andWithCriterion(new Criterion\ParentLocationId(2))
    ->withSortClause(new SortClause\DatePublished(Query::SORT_ASC))
    ->sliceBy(10, 2);

Search Criteria and Sort Clause availability

Not all Search Criteria and Sort Clauses are available for use in Repository filtering.

Only Criteria implementing FilteringCriterion and Sort Clauses implementing FilteringSortClause are supported.

See Search Criteria and Sort Clause reference for details.

Tip

It is recommended to use an IDE that can recognize type hints when working with Repository Filtering. If you try to use an unsupported Criterion or Sort Clause, the IDE will indicate an issue.

Searching in a controller

You can use the SearchService or Repository filtering in a controller, as long as you provide the required parameters. For example, in the code below, locationId is provided to list all children of a Location by using the SearchService.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//...
use eZ\Publish\API\Repository\SearchService;
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;

class CustomController extends Controller
{
    //...
    public function showContentAction($locationId)
    {
        $query = new LocationQuery();
        $query->filter = new Criterion\ParentLocationId($locationId);

        $results = $this->searchService->findContentInfo($query);
        $items = [];
        foreach ($results->searchHits as $searchHit) {
            $items[] = $searchHit;
        }

        return $this->render('custom.html.twig', [
            'items' => $items,
        ]);
    }
}

The rendering of results is then relegated to templates (lines 20-22).

When using Repository filtering, provide the results of ContentService::find() as parameters to the view:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...
use eZ\Publish\API\Repository\ContentService;
use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\ParentLocationId;
use eZ\Publish\API\Repository\Values\Filter\Filter;
use eZ\Publish\Core\MVC\Symfony\View\ContentView;

class CustomController extends Controller
{
    // ...
    public function showChildrenAction(ContentView $view): ContentView
    {
        $filter = new Filter();
        $filter
            ->withCriterion(new ParentLocationId($view->getLocation()->id))

        $view->setParameters(
            [
                'items' => $this->contentService->find($filter),
            ]
        );

        return $view;
    }
}

Paginating search results

To paginate search or filtering results, it is recommended to use the Pagerfanta library and Ibexa Platform's adapters for it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//...
use eZ\Publish\Core\Pagination\Pagerfanta\ContentSearchAdapter;
use Symfony\Component\HttpFoundation\Request;
use Pagerfanta\Pagerfanta;

class CustomController extends Controller
{
    //...
    public function showContentAction(Request $request, $locationId)
    {
        // formulate a $query

        $pager = new Pagerfanta(
            new ContentSearchAdapter($query, $this->searchService)
        );
        $pager->setMaxPerPage(3);
        $pager->setCurrentPage($request->get('page', 1));

        return $this->render('custom.html.twig', [
                'totalItemCount' => $pager->getNbResults(),
                'pagerItems' => $pager,
            ]
        );
    }
}

Pagination can then be rendered for example using the following template:

1
2
3
4
5
6
7
{% for item in pagerItems %}
    <h2><a href={{ ez_path(item.valueObject) }}>{{ ez_content_name(item) }}</a></h2>
{% endfor %}

{% if pagerItems.haveToPaginate() %}
    {{ pagerfanta( pagerItems, 'ez') }}
{% endif %}

For more information and examples, see PagerFanta documentation.

Pagerfanta adapters

Adapter class name Description
ContentSearchAdapter Makes a search against passed Query and returns Content objects.
ContentSearchHitAdapter Makes a search against passed Query and returns SearchHit objects instead.
LocationSearchAdapter Makes a Location search against passed Query and returns Location objects.
LocationSearchHitAdapter Makes a Location search against passed Query and returns SearchHit objects instead.
ContentFilteringAdapter Applies a Content filter and returns a ContentList object.
LocationFilteringAdapter Applies a Location filter and returns a LocationList object.

For more complex searches, you need to combine multiple Criteria. You can do it using logical operators: LogicalAnd, LogicalOr, and LogicalNot.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$query = new LocationQuery;
$criterion1 = new Criterion\Subtree($this->locationService->loadLocation($locationId)->pathString);
$criterion2 = new Criterion\ContentTypeId($contentTypeId);
$criterion3 = new Criterion\FullText($text);

$query->query = new Criterion\LogicalAnd(
    [$criterion1, $criterion2, $criterion3]
);

$result = $this->searchService->findContentInfo($query);
$output->writeln('Found ' . $result->totalCount . ' items');
foreach ($result->searchHits as $searchHit) {
    $output->writeln($searchHit->valueObject->name);
}

This example takes three parameters from a command — $text, $contentTypeId, and $locationId. It then combines them using Criterion\LogicalAnd to search for Content items that belong to a specific subtree, have the chosen Content Type and contain the provided text (lines 6-8).

This also shows that you can get the total number of search results using the totalCount property of search results (line 11).

You can also nest different operators to construct more complex queries. The example below uses the LogicalNot operator to search for all children of the selected parent that do not belong to the provided Content Type:

1
2
3
4
5
6
7
$query->filter = new Criterion\LogicalAnd([
        new Criterion\ParentLocationId($locationId),
        new Criterion\LogicalNot(
            new Criterion\ContentTypeIdentifier($contentTypeId)
        )
    ]
);

Combining independent Criteria

Criteria are independent of one another. This can lead to unexpected behavior, for instance because content can have multiple Locations.

For example, a Content item has two Locations: visible Location A and hidden Location B. You perform the following query:

1
2
3
4
$query->filter = new Criterion\LogicalAnd([
    new LocationId($locationBId),
    new Visibility(Visibility::VISIBLE),
]);

The query searches for Location B using the LocationId Criterion, and for visible content using the Visibility Criterion.

Even though the Location B is hidden, the query will find the content because both conditions are satisfied:

  • the Content item has Location B
  • the Content item is visible (it has the visible Location A)

Sorting results

To sort the results of a query, use one of more Sort Clauses.

For example, to order search results by their publicationg date, from oldest to newest, and then alphabetically by content name, add the following Sort Clauses to the query:

1
2
3
4
$query->sortClauses = [
    new SortClause\DatePublished(LocationQuery::SORT_ASC),
    new SortClause\ContentName(LocationQuery::SORT_DESC),
];

Tip

For the full list and details of available Sort Clauses, see Sort Clause reference.

Searching in trash

In the user interface, on the Trash screen, you can search for Content items, and then sort the results based on different criteria. To search the trash with the API, use the TrashService::findInTrash method to submit a query for Content items that are held in trash. Searching in trash supports a limited set of Criteria and Sort Clauses. For a list of supported Criteria and Sort Clauses, see Searching in trash reference.

Note

Searching through the trashed Content items operates directly on the database, therefore you cannot use external search engines, such as Solr or Elasticsearch, and it is impossible to reindex the data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use eZ\Publish\API\Repository\Values\Content\Query;

    // ...

    $query = new Query();
    // find trashed folders
    $query->filter = new Query\Criterion\ContentTypeId([1]);
    $results = $this->trashService->findTrashItems($query);
    foreach ($results->items as $trashedLocation) {
        /** @var \eZ\Publish\API\Repository\Values\Content\TrashItem[] $trashedLocation */
        // ...
    }

Caution

Make sure that you set the Criterion on the filter property. It is impossible to use the query property, because the search in trash operation filters the database instead of querying.

Aggregation

Feature support

Aggregation is only available in the Solr and Elasticsearch search engines.

With aggregations you can find the count of search results or other result information for each Aggregation type.

To do this, you use of the query's $aggregations property:

1
$query->aggregations[] = new ContentTypeTermAggregation('content_type');

The name of the aggregation must be unique in the given query.

Access the results by using the get() method of the aggregation:

1
$contentByType = $results->aggregations->get('content_type');

Aggregation results contain the name of the result and the count of found items:

1
2
3
foreach ($contentByType as $contentType => $count) {
    $output->writeln($contentType->getName() . ': ' . $count);
}

With field aggregations you can group search results according to the value of a specific Field. In this case the aggregation takes the Content Type identifier and the Field identifier as parameters.

The following example creates an aggregation named selection that groups results according to the value of the topic Field in the article Content Type:

1
$query->aggregations[] = new SelectionTermAggregation('selection', 'article', 'topic');

With term aggregation you can define additional limits to the results. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results:

1
2
$query->aggregations[0]->setLimit(5);
$query->aggregations[0]->setMinCount(10);

To use a range aggregation, you must provide a ranges array containing a set of Range objects that define the borders of the specific range sets.

1
2
3
4
5
6
$query->aggregations[] = new IntegerRangeAggregation('range', 'person', 'age',
[
    new Query\Aggregation\Range(1,30),
    new Query\Aggregation\Range(30,60),
    new Query\Aggregation\Range(60,null),
]);

Note

The beginning of the range is included and the end is excluded, so a range between 1 and 30 will include value 1, but not 30.

null means that a range does not have an end. In the example all values above (and including) 60 are included in the last range.

See Agrregation reference for details of all available aggregations.

Deprecated

Search Facets are deprecated since version v3.2.

Checking feature support per search engine

Faceted search is available only for the Solr search engine.

To find out if a given search engine supports any of the advanced search capabilities, use the eZ\Publish\API\Repository\SearchService::supports method:

1
$facetSupport = $this->searchService->supports(SearchService::CAPABILITY_FACETS);

Faceted search enables you to find the count of search results for each Facet value.

To do this, you need to make use of the query's $facetBuilders property:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$query->facetBuilders[] = new FacetBuilderUserFacetBuilder(
    [
        'name' => 'User',
        'type' => FacetBuilder\UserFacetBuilder::OWNER,
        'minCount' => 2,
        'limit' => 5
    ]
);

$result = $this->searchService->findContentInfo($query);

$output->writeln("Number of results per facet value: ");
foreach ($result->facets[0]->entries as $facetEntry) {
    $output->writeln("* " . $facetEntry);
}

See Search Facet reference for details of all available Facets.