Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add highlight methods to search and search builder #349

Merged
merged 10 commits into from
Feb 25, 2025
Merged
31 changes: 29 additions & 2 deletions docs/search-and-filters/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ The field is required to be marked as ``filterable`` in the index configuration.

.. note::

The ``GeoBoundingBoxCondition`` is currently not supported by ``Redisearch`` adapter.
See `this GitHub Issue <https://github.com/RediSearch/RediSearch/issues/680>`__ for more information.
The ``GeoBoundingBoxCondition`` is currently not supported by ``RediSearch`` adapter.
See `this GitHub Issue <https://github.com/PHP-CMSIG/search/issues/422>`__ for more information.

OrCondition
~~~~~~~~~~~
Expand Down Expand Up @@ -396,6 +396,33 @@ The field is required to be marked as ``sortable`` in the index configuration.

--------------

Highlighting
------------

The abstraction can also be used to highlight the search term in the result.

.. code-block:: php

<?php

use CmsIg\Seal\Search\Condition;

$result = $this->engine->createSearchBuilder('blog')
->addFilter(new Condition\SearchCondition('Search Term'))
->highlight(['title'], '<mark>', '</mark>');
->getResult();

foreach ($result as $document) {
$titleWithHighlight = $document['_formatted']['title'] ?? '';
}

.. note::

The ``Highlighting`` is currently not supported by ``RediSearch`` adapter.
See `this GitHub Issue <https://github.com/PHP-CMSIG/search/issues/491>`__ for more information.

--------------

Summary
-------

Expand Down
46 changes: 40 additions & 6 deletions packages/seal-algolia-adapter/src/AlgoliaSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ public function search(Search $search): Result
);
} catch (NotFoundException) {
return new Result(
$this->hitsToDocuments($search->index, []),
$this->hitsToDocuments($search->index, [], []),
0,
);
}

return new Result(
$this->hitsToDocuments($search->index, [$data]),
$this->hitsToDocuments($search->index, [$data], []),
1,
);
}
Expand Down Expand Up @@ -106,29 +106,63 @@ public function search(Search $search): Result
$searchParams['query'] = $query;
}

if ([] !== $search->highlightFields) {
$searchParams['attributesToHighlight'] = $search->highlightFields;
$searchParams['highlightPreTag'] = $search->highlightPreTag;
$searchParams['highlightPostTag'] = $search->highlightPostTag;
}

$data = $this->client->searchSingleIndex($indexName, $searchParams);
\assert(\is_array($data) && isset($data['hits']) && \is_array($data['hits']), 'The "hits" array is expected to be returned by algolia client.');
\assert(isset($data['nbHits']) && \is_int($data['nbHits']), 'The "nbHits" value is expected to be returned by algolia client.');

return new Result(
$this->hitsToDocuments($search->index, $data['hits']),
$this->hitsToDocuments($search->index, $data['hits'], $search->highlightFields),
$data['nbHits'] ?? null, // @phpstan-ignore-line
);
}

/**
* @param iterable<array<string, mixed>> $hits
* @param array<string> $highlightFields
*
* @return \Generator<int, array<string, mixed>>
*/
private function hitsToDocuments(Index $index, iterable $hits): \Generator
private function hitsToDocuments(Index $index, iterable $hits, array $highlightFields): \Generator
{
foreach ($hits as $hit) {
// remove Algolia Metadata
unset($hit['objectID']);
unset($hit['_highlightResult']);

yield $this->marshaller->unmarshall($index->fields, $hit);
$document = $this->marshaller->unmarshall($index->fields, $hit);

if ([] === $highlightFields) {
yield $document;

continue;
}

$document['_formatted'] ??= [];

\assert(
\is_array($document['_formatted']),
'Document with key "_formatted" expected to be array.',
);

foreach ($highlightFields as $highlightField) {
\assert(
isset($hit['_highlightResult'])
&& \is_array($hit['_highlightResult'])
&& isset($hit['_highlightResult'][$highlightField])
&& \is_array($hit['_highlightResult'][$highlightField])
&& isset($hit['_highlightResult'][$highlightField]['value']),
'Expected highlight field to be set.',
);

$document['_formatted'][$highlightField] = $hit['_highlightResult'][$highlightField]['value'];
}

yield $document;
}
}

Expand Down
54 changes: 48 additions & 6 deletions packages/seal-elasticsearch-adapter/src/ElasticsearchSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ public function search(Search $search): Result
}

return new Result(
$this->hitsToDocuments($search->index, []),
$this->hitsToDocuments($search->index, [], []),
0,
);
}

return new Result(
$this->hitsToDocuments($search->index, [$searchResult]),
$this->hitsToDocuments($search->index, [$searchResult], []),
1,
);
}
Expand Down Expand Up @@ -99,6 +99,20 @@ public function search(Search $search): Result
$body['size'] = $search->limit;
}

if ([] !== $search->highlightFields) {
$highlightFields = [];
foreach ($search->highlightFields as $highlightField) {
$highlightFields[$highlightField] = [
'pre_tags' => [$search->highlightPreTag],
'post_tags' => [$search->highlightPostTag],
];
}

$body['highlight'] = [
'fields' => $highlightFields,
];
}

/** @var Elasticsearch $response */
$response = $this->client->search([
'index' => $search->index->name,
Expand All @@ -118,21 +132,49 @@ public function search(Search $search): Result
$searchResult = $response->asArray();

return new Result(
$this->hitsToDocuments($search->index, $searchResult['hits']['hits']),
$this->hitsToDocuments($search->index, $searchResult['hits']['hits'], $search->highlightFields),
$searchResult['hits']['total']['value'],
);
}

/**
* @param array<array<string, mixed>> $hits
* @param array<string> $highlightFields
*
* @return \Generator<int, array<string, mixed>>
*/
private function hitsToDocuments(Index $index, array $hits): \Generator
private function hitsToDocuments(Index $index, array $hits, array $highlightFields): \Generator
{
/** @var array{_index: string, _source: array<string, mixed>} $hit */
/** @var array{_index: string, _source: array<string, mixed>, highlight?: mixed} $hit */
foreach ($hits as $hit) {
yield $this->marshaller->unmarshall($index->fields, $hit['_source']);
$document = $this->marshaller->unmarshall($index->fields, $hit['_source']);

if ([] === $highlightFields) {
yield $document;

continue;
}

$document['_formatted'] ??= [];

\assert(
\is_array($document['_formatted']),
'Document with key "_formatted" expected to be array.',
);

foreach ($highlightFields as $highlightField) {
\assert(
isset($hit['highlight'])
&& \is_array($hit['highlight'])
&& isset($hit['highlight'][$highlightField])
&& \is_array($hit['highlight'][$highlightField]),
'Expected highlight field to be set.',
);

$document['_formatted'][$highlightField] = $hit['highlight'][$highlightField][0] ?? null;
}

yield $document;
}
}

Expand Down
45 changes: 40 additions & 5 deletions packages/seal-loupe-adapter/src/LoupeSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ public function search(Search $search): Result

if (!$data) {
return new Result(
$this->hitsToDocuments($search->index, []),
$this->hitsToDocuments($search->index, [], []),
0,
);
}

return new Result(
$this->hitsToDocuments($search->index, [$data]),
$this->hitsToDocuments($search->index, [$data], []),
1,
);
}
Expand All @@ -83,6 +83,14 @@ public function search(Search $search): Result
$searchParameters = $searchParameters->withHitsPerPage($search->limit);
}

if ([] !== $search->highlightFields) {
$searchParameters = $searchParameters->withAttributesToHighlight(
$search->highlightFields,
$search->highlightPreTag,
$search->highlightPostTag,
);
}

if ($search->offset && $search->limit && 0 === ($search->offset % $search->limit)) {
$searchParameters = $searchParameters->withPage((int) (($search->offset / $search->limit) + 1));
} elseif (null !== $search->limit && 0 !== $search->offset) {
Expand All @@ -101,7 +109,7 @@ public function search(Search $search): Result
$result = $loupe->search($searchParameters);

return new Result(
$this->hitsToDocuments($search->index, $result->getHits()),
$this->hitsToDocuments($search->index, $result->getHits(), $search->highlightFields),
$result->getTotalHits(),
);
}
Expand All @@ -113,13 +121,40 @@ private function escapeFilterValue(string|int|float|bool $value): string

/**
* @param iterable<array<string, mixed>> $hits
* @param array<string> $highlightFields
*
* @return \Generator<int, array<string, mixed>>
*/
private function hitsToDocuments(Index $index, iterable $hits): \Generator
private function hitsToDocuments(Index $index, iterable $hits, array $highlightFields): \Generator
{
foreach ($hits as $hit) {
yield $this->marshaller->unmarshall($index->fields, $hit);
$document = $this->marshaller->unmarshall($index->fields, $hit);

if ([] === $highlightFields) {
yield $document;

continue;
}

$document['_formatted'] ??= [];

\assert(
\is_array($document['_formatted']),
'Document with key "_formatted" expected to be array.',
);

foreach ($highlightFields as $highlightField) {
\assert(
isset($hit['_formatted'])
&& \is_array($hit['_formatted'])
&& isset($hit['_formatted'][$highlightField]),
'Expected highlight field to be set.',
);

$document['_formatted'][$highlightField] = $hit['_formatted'][$highlightField];
}

yield $document;
}
}

Expand Down
43 changes: 38 additions & 5 deletions packages/seal-meilisearch-adapter/src/MeilisearchSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ public function search(Search $search): Result
}

return new Result(
$this->hitsToDocuments($search->index, []),
$this->hitsToDocuments($search->index, [], []),
0,
);
}

return new Result(
$this->hitsToDocuments($search->index, [$data]),
$this->hitsToDocuments($search->index, [$data], []),
1,
);
}
Expand All @@ -88,23 +88,56 @@ public function search(Search $search): Result
$searchParams['sort'][] = $field . ':' . $direction;
}

if ([] !== $search->highlightFields) {
$searchParams['attributesToHighlight'] = $search->highlightFields;
$searchParams['highlightPreTag'] = $search->highlightPreTag;
$searchParams['highlightPostTag'] = $search->highlightPostTag;
}

$data = $searchIndex->search($query, $searchParams)->toArray();

return new Result(
$this->hitsToDocuments($search->index, $data['hits']),
$this->hitsToDocuments($search->index, $data['hits'], $search->highlightFields),
$data['totalHits'] ?? $data['estimatedTotalHits'] ?? null,
);
}

/**
* @param iterable<array<string, mixed>> $hits
* @param array<string> $highlightFields
*
* @return \Generator<int, array<string, mixed>>
*/
private function hitsToDocuments(Index $index, iterable $hits): \Generator
private function hitsToDocuments(Index $index, iterable $hits, array $highlightFields): \Generator
{
foreach ($hits as $hit) {
yield $this->marshaller->unmarshall($index->fields, $hit);
$document = $this->marshaller->unmarshall($index->fields, $hit);

if ([] === $highlightFields) {
yield $document;

continue;
}

$document['_formatted'] ??= [];

\assert(
\is_array($document['_formatted']),
'Document with key "_formatted" expected to be array.',
);

foreach ($highlightFields as $highlightField) {
\assert(
isset($hit['_formatted'])
&& \is_array($hit['_formatted'])
&& isset($hit['_formatted'][$highlightField]),
'Expected highlight field to be set.',
);

$document['_formatted'][$highlightField] = $hit['_formatted'][$highlightField];
}

yield $document;
}
}

Expand Down
Loading
Loading