Skip to content

Commit

Permalink
Remove support for search multi Indexes by once (BC Break) (#451)
Browse files Browse the repository at this point in the history
As discuss result of the current state in #99 we will not longer support
search on multi indexes as most engine do not support that. As an abstraction we should support what is possible on all or most engines. See #99.

# BC Breaks

- The `Engine::createSearchBuilder` requires a parameter `string $index`
- The `Search` object not longer has `indexes` instead a single `index` variable
- Elasticsearch and Opensearch can not longer search on multiple indexes
in an aggregated result
  • Loading branch information
alexander-schranz authored Oct 21, 2024
1 parent a631d99 commit d91446a
Show file tree
Hide file tree
Showing 23 changed files with 199 additions and 416 deletions.
42 changes: 23 additions & 19 deletions packages/seal-algolia-adapter/bin/drop_all_indexes.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,35 @@

$_ENV['ALGOLIA_DSN'] = $algoliaDsn;

$client = \Schranz\Search\SEAL\Adapter\Algolia\Tests\ClientHelper::getClient();

$return = 0;

$retryIndexes = [];
foreach ($client->listIndices()['items'] as $key => $value) {
echo 'Delete ... ' . $value['name'] . \PHP_EOL;
$client = \Schranz\Search\SEAL\Adapter\Algolia\Tests\ClientHelper::getClient();
$retryIndexes = $client->listIndices()['items'];
$retryCounter = 0;

while (\count($retryIndexes) > 0) {
$client = \Schranz\Search\SEAL\Adapter\Algolia\Tests\ClientHelper::getClient();
$currentIndexes = $retryIndexes;
$retryIndexes = [];
foreach ($currentIndexes as $key => $value) {
echo 'Delete ... ' . $value['name'] . \PHP_EOL;

try {
$client->deleteIndex($value['name']);
} catch (\Exception) {
$retryIndexes[$key] = $value;
echo 'Retry later ... ' . $value['name'] . \PHP_EOL;
}
}

try {
$client->deleteIndex($value['name']);
} catch (\Exception) {
$retryIndexes[$key] = $value;
echo 'Retry later ... ' . $value['name'] . \PHP_EOL;
++$retryCounter;
if ($retryCounter >= 10) {
break;
}
}

foreach ($retryIndexes as $key => $value) {
echo 'Delete ... ' . $value['name'] . \PHP_EOL;

try {
$client->deleteIndex($value['name']);
} catch (\Exception) {
echo 'Errored ... ' . $value['name'] . \PHP_EOL;
$return = 1;
}
if (\count($retryIndexes) > 0) {
$return = 1;
}

exit($return);
27 changes: 8 additions & 19 deletions packages/seal-algolia-adapter/src/AlgoliaSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,43 +42,35 @@ public function search(Search $search): Result
{
// optimized single document query
if (
1 === \count($search->indexes)
&& 1 === \count($search->filters)
1 === \count($search->filters)
&& $search->filters[0] instanceof Condition\IdentifierCondition
&& 0 === $search->offset
&& 1 === $search->limit
) {
$index = $search->indexes[\array_key_first($search->indexes)];

try {
/** @var array<string, mixed> $data */
$data = $this->client->getObject(
$index->name,
$search->index->name,
$search->filters[0]->identifier,
);
} catch (NotFoundException) {
return new Result(
$this->hitsToDocuments($search->indexes, []),
$this->hitsToDocuments($search->index, []),
0,
);
}

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

if (1 !== \count($search->indexes)) {
throw new \RuntimeException('Algolia Adapter does not yet support search multiple indexes: https://github.com/schranz-search/schranz-search/issues/41');
}

if (\count($search->sortBys) > 1) {
throw new \RuntimeException('Algolia Adapter does not yet support search multiple indexes: https://github.com/schranz-search/schranz-search/issues/41');
}

$index = $search->indexes[\array_key_first($search->indexes)];
$indexName = $index->name;
$indexName = $search->index->name;

$sortByField = \array_key_first($search->sortBys);
if ($sortByField) {
Expand All @@ -87,7 +79,7 @@ public function search(Search $search): Result

$query = '';
$geoFilters = [];
$filters = $this->recursiveResolveFilterConditions($index, $search->filters, true, $query, $geoFilters);
$filters = $this->recursiveResolveFilterConditions($search->index, $search->filters, true, $query, $geoFilters);

$searchParams = [];
if ('' !== $filters) {
Expand Down Expand Up @@ -119,21 +111,18 @@ public function search(Search $search): Result
\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->indexes, $data['hits']),
$this->hitsToDocuments($search->index, $data['hits']),
$data['nbHits'] ?? null, // @phpstan-ignore-line
);
}

/**
* @param Index[] $indexes
* @param iterable<array<string, mixed>> $hits
*
* @return \Generator<int, array<string, mixed>>
*/
private function hitsToDocuments(array $indexes, iterable $hits): \Generator
private function hitsToDocuments(Index $index, iterable $hits): \Generator
{
$index = $indexes[\array_key_first($indexes)];

foreach ($hits as $hit) {
// remove Algolia Metadata
unset($hit['objectID']);
Expand Down
8 changes: 0 additions & 8 deletions packages/seal-algolia-adapter/tests/AlgoliaSearcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,4 @@ public static function setUpBeforeClass(): void

parent::setUpBeforeClass();
}

/**
* @doesNotPerformAssertions
*/
public function testFindMultipleIndexes(): void
{
$this->markTestSkipped('Not supported by Algolia: https://github.com/schranz-search/schranz-search/issues/41');
}
}
80 changes: 25 additions & 55 deletions packages/seal-elasticsearch-adapter/src/ElasticsearchSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
use Elastic\Elasticsearch\Response\Elasticsearch;
use Schranz\Search\SEAL\Adapter\SearcherInterface;
use Schranz\Search\SEAL\Marshaller\Marshaller;
use Schranz\Search\SEAL\Schema\Exception\FieldByPathNotFoundException;
use Schranz\Search\SEAL\Schema\Field;
use Schranz\Search\SEAL\Schema\Index;
use Schranz\Search\SEAL\Search\Condition;
Expand All @@ -44,16 +43,15 @@ public function search(Search $search): Result
{
// optimized single document query
if (
1 === \count($search->indexes)
&& 1 === \count($search->filters)
1 === \count($search->filters)
&& $search->filters[0] instanceof Condition\IdentifierCondition
&& 0 === $search->offset
&& 1 === $search->limit
) {
try {
/** @var Elasticsearch $response */
$response = $this->client->get([
'index' => $search->indexes[\array_key_first($search->indexes)]->name,
'index' => $search->index->name,
'id' => $search->filters[0]->identifier,
]);

Expand All @@ -66,23 +64,18 @@ public function search(Search $search): Result
}

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

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

$indexesNames = [];
foreach ($search->indexes as $index) {
$indexesNames[] = $index->name;
}

$query = $this->recursiveResolveFilterConditions($search->indexes, $search->filters, true);
$query = $this->recursiveResolveFilterConditions($search->index, $search->filters, true);

if ([] === $query) {
$query['match_all'] = new \stdClass();
Expand All @@ -108,7 +101,7 @@ public function search(Search $search): Result

/** @var Elasticsearch $response */
$response = $this->client->search([
'index' => \implode(',', $indexesNames),
'index' => $search->index->name,
'body' => $body,
]);

Expand All @@ -125,87 +118,64 @@ public function search(Search $search): Result
$searchResult = $response->asArray();

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

/**
* @param Index[] $indexes
* @param array<array<string, mixed>> $hits
*
* @return \Generator<int, array<string, mixed>>
*/
private function hitsToDocuments(array $indexes, array $hits): \Generator
private function hitsToDocuments(Index $index, array $hits): \Generator
{
$indexesByInternalName = [];
foreach ($indexes as $index) {
$indexesByInternalName[$index->name] = $index;
}

/** @var array{_index: string, _source: array<string, mixed>} $hit */
foreach ($hits as $hit) {
$index = $indexesByInternalName[$hit['_index']] ?? null;
if (!$index instanceof Index) {
throw new \RuntimeException('SchemaMetadata for Index "' . $hit['_index'] . '" not found.');
}

yield $this->marshaller->unmarshall($index->fields, $hit['_source']);
}
}

/**
* @param Index[] $indexes
*/
private function getFilterField(array $indexes, string $name): string
private function getFilterField(Index $index, string $name): string
{
foreach ($indexes as $index) {
try {
$field = $index->getFieldByPath($name);
$field = $index->getFieldByPath($name);

if ($field instanceof Field\TextField) {
return $name . '.raw';
}

return $name;
} catch (FieldByPathNotFoundException) {
// ignore when field is not found and use go to next index instead
}
if ($field instanceof Field\TextField) {
return $name . '.raw';
}

return $name;
}

/**
* @param Index[] $indexes
* @param object[] $filters
*
* @return array<string|int, mixed>
*/
private function recursiveResolveFilterConditions(array $indexes, array $filters, bool $conjunctive): array
private function recursiveResolveFilterConditions(Index $index, array $filters, bool $conjunctive): array
{
$filterQueries = [];

foreach ($filters as $filter) {
match (true) {
$filter instanceof Condition\IdentifierCondition => $filterQueries[]['ids']['values'][] = $filter->identifier,
$filter instanceof Condition\SearchCondition => $filterQueries[]['bool']['must']['query_string']['query'] = $filter->query,
$filter instanceof Condition\EqualCondition => $filterQueries[]['term'][$this->getFilterField($indexes, $filter->field)]['value'] = $filter->value,
$filter instanceof Condition\NotEqualCondition => $filterQueries[]['bool']['must_not']['term'][$this->getFilterField($indexes, $filter->field)]['value'] = $filter->value,
$filter instanceof Condition\GreaterThanCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['gt'] = $filter->value,
$filter instanceof Condition\GreaterThanEqualCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['gte'] = $filter->value,
$filter instanceof Condition\LessThanCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['lt'] = $filter->value,
$filter instanceof Condition\LessThanEqualCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['lte'] = $filter->value,
$filter instanceof Condition\InCondition, => $filterQueries[]['terms'][$this->getFilterField($indexes, $filter->field)] = $filter->values,
$filter instanceof Condition\NotInCondition => $filterQueries[]['bool']['must_not']['terms'][$this->getFilterField($indexes, $filter->field)] = $filter->values,
$filter instanceof Condition\EqualCondition => $filterQueries[]['term'][$this->getFilterField($index, $filter->field)]['value'] = $filter->value,
$filter instanceof Condition\NotEqualCondition => $filterQueries[]['bool']['must_not']['term'][$this->getFilterField($index, $filter->field)]['value'] = $filter->value,
$filter instanceof Condition\GreaterThanCondition => $filterQueries[]['range'][$this->getFilterField($index, $filter->field)]['gt'] = $filter->value,
$filter instanceof Condition\GreaterThanEqualCondition => $filterQueries[]['range'][$this->getFilterField($index, $filter->field)]['gte'] = $filter->value,
$filter instanceof Condition\LessThanCondition => $filterQueries[]['range'][$this->getFilterField($index, $filter->field)]['lt'] = $filter->value,
$filter instanceof Condition\LessThanEqualCondition => $filterQueries[]['range'][$this->getFilterField($index, $filter->field)]['lte'] = $filter->value,
$filter instanceof Condition\InCondition, => $filterQueries[]['terms'][$this->getFilterField($index, $filter->field)] = $filter->values,
$filter instanceof Condition\NotInCondition => $filterQueries[]['bool']['must_not']['terms'][$this->getFilterField($index, $filter->field)] = $filter->values,
$filter instanceof Condition\GeoDistanceCondition => $filterQueries[]['geo_distance'] = [
'distance' => $filter->distance,
$this->getFilterField($indexes, $filter->field) => [
$this->getFilterField($index, $filter->field) => [
'lat' => $filter->latitude,
'lon' => $filter->longitude,
],
],
$filter instanceof Condition\GeoBoundingBoxCondition => $filterQueries[]['geo_bounding_box'][$this->getFilterField($indexes, $filter->field)] = [
$filter instanceof Condition\GeoBoundingBoxCondition => $filterQueries[]['geo_bounding_box'][$this->getFilterField($index, $filter->field)] = [
'top_left' => [
'lat' => $filter->northLatitude,
'lon' => $filter->westLongitude,
Expand All @@ -215,8 +185,8 @@ private function recursiveResolveFilterConditions(array $indexes, array $filters
'lon' => $filter->eastLongitude,
],
],
$filter instanceof Condition\AndCondition => $filterQueries[] = $this->recursiveResolveFilterConditions($indexes, $filter->conditions, true),
$filter instanceof Condition\OrCondition => $filterQueries[] = $this->recursiveResolveFilterConditions($indexes, $filter->conditions, false),
$filter instanceof Condition\AndCondition => $filterQueries[] = $this->recursiveResolveFilterConditions($index, $filter->conditions, true),
$filter instanceof Condition\OrCondition => $filterQueries[] = $this->recursiveResolveFilterConditions($index, $filter->conditions, false),
default => throw new \LogicException($filter::class . ' filter not implemented.'),
};
}
Expand Down
Loading

0 comments on commit d91446a

Please sign in to comment.