From 712ac923ade6fe271cddd2021e54465e73c51c3a Mon Sep 17 00:00:00 2001 From: k-samuel Date: Fri, 31 Dec 2021 06:02:21 +0300 Subject: [PATCH 1/6] 2.1.0 --- README.md | 73 +++- changelog.md | 30 ++ composer.json | 2 +- src/Filter/RangeFilter.php | 12 +- src/Index.php | 157 +------- src/Index/ArrayIndex.php | 193 ++++++++++ src/Index/FixedArrayIndex.php | 252 +++++++++++++ src/Index/IndexInterface.php | 96 +++++ src/Search.php | 11 +- src/Sorter/ByField.php | 11 +- .../{SearchBench.php => ArrayIndexBench.php} | 12 +- .../Bench/{ => ArrayIndex}/S100kBench.php | 6 +- .../Bench/{ => ArrayIndex}/S10kBench.php | 6 +- .../Bench/{ => ArrayIndex}/S1mBench.php | 6 +- .../Bench/{ => ArrayIndex}/S300kBench.php | 6 +- .../Bench/{ => ArrayIndex}/S50kBench.php | 6 +- .../Bench/FixedArrayIndex/S100kBench.php | 29 ++ .../Bench/FixedArrayIndex/S10kBench.php | 29 ++ .../Bench/FixedArrayIndex/S1mBench.php | 29 ++ .../Bench/FixedArrayIndex/S300kBench.php | 29 ++ .../Bench/FixedArrayIndex/S50kBench.php | 29 ++ tests/benchmark/DatasetFactory.php | 32 +- tests/benchmark/FixedArrayIndexBench.php | 40 ++ tests/performance/find.php | 10 +- tests/performance/findFixed.php | 90 +++++ tests/unit/IndexTest.php | 21 +- .../unit/Indexer/Number/RangeIndexerTest.php | 22 +- .../Indexer/Number/RangeListIndexerTest.php | 22 +- ...earchTest.php => SearchArrayIndexTest.php} | 2 +- tests/unit/SearchFixedArrayIndexTest.php | 353 ++++++++++++++++++ tests/unit/Sorter/FieldTest.php | 36 +- 31 files changed, 1419 insertions(+), 233 deletions(-) create mode 100644 src/Index/ArrayIndex.php create mode 100644 src/Index/FixedArrayIndex.php create mode 100644 src/Index/IndexInterface.php rename tests/benchmark/{SearchBench.php => ArrayIndexBench.php} (92%) rename tests/benchmark/Bench/{ => ArrayIndex}/S100kBench.php (77%) rename tests/benchmark/Bench/{ => ArrayIndex}/S10kBench.php (77%) rename tests/benchmark/Bench/{ => ArrayIndex}/S1mBench.php (77%) rename tests/benchmark/Bench/{ => ArrayIndex}/S300kBench.php (77%) rename tests/benchmark/Bench/{ => ArrayIndex}/S50kBench.php (77%) create mode 100644 tests/benchmark/Bench/FixedArrayIndex/S100kBench.php create mode 100644 tests/benchmark/Bench/FixedArrayIndex/S10kBench.php create mode 100644 tests/benchmark/Bench/FixedArrayIndex/S1mBench.php create mode 100644 tests/benchmark/Bench/FixedArrayIndex/S300kBench.php create mode 100644 tests/benchmark/Bench/FixedArrayIndex/S50kBench.php create mode 100644 tests/benchmark/FixedArrayIndexBench.php create mode 100644 tests/performance/findFixed.php rename tests/unit/{SearchTest.php => SearchArrayIndexTest.php} (99%) create mode 100644 tests/unit/SearchFixedArrayIndexTest.php diff --git a/README.md b/README.md index d73f121..a65d7bf 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ For example: list of ProductId "in stock" to exclude not available products. Tests on sets of products with 10 attributes, search with filters by 3 fields. -PHPBench v2.0.3 PHP 8.1.0 + JIT + opcache (no xdebug extension) +PHPBench v2.1.0 ArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | |----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| @@ -66,15 +66,15 @@ PHPBench v2.0.3 PHP 8.1.0 + JIT + opcache (no xdebug extension) | 300,000 | ~189Mb | ~0.011 s. | ~0.108 s. | ~0.005 s. | 26891 | | 1,000,000 | ~657Mb | ~0.052 s. | ~0.419 s. | ~0.018 s. | 90520 | -Bench v1.3.3 PHP 8.1.0 + JIT + opcache (no xdebug extension) +PHPBench v2.1.0 FixedArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | |----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| -| 10,000 | ~7Mb | ~0.0007 s. | ~0.004 s. | ~0.0003 s. | 907 | -| 50,000 | ~49Mb | ~0.002 s. | ~0.014 s. | ~0.0009 s. | 4550 | -| 100,000 | ~98Mb | ~0.004 s. | ~0.028 s. | ~0.002 s. | 8817 | -| 300,000 | ~242Mb | ~0.012 s. | ~0.112 s. | ~0.007 s. | 26891 | -| 1,000,000 | ~812Mb | ~0.057 s. | ~0.443 s. | ~0.034 s. | 90520 | +| 10,000 | ~2Mb | ~0.0007 s. | ~0.006 s. | ~0.0002 s. | 907 | +| 50,000 | ~12Mb | ~0.003 s. | ~0.029 s. | ~0.001 s. | 4550 | +| 100,000 | ~23Mb | ~0.007 s. | ~0.058 s. | ~0.002 s. | 8817 | +| 300,000 | ~70Mb | ~0.021 s. | ~0.191 s. | ~0.008 s. | 26891 | +| 1,000,000 | ~233Mb | ~0.081 s. | ~0.702 s. | ~0.033 s. | 90520 | * Items count - Products in index * Memory - RAM used for index @@ -101,9 +101,9 @@ Bench v0.3.1 golang 1.17.3 with parallel aggregates Create index using console/crontab etc. ```php addRecord it can improve performance @@ -115,7 +115,7 @@ $data = [ ]; foreach($data as $item){ $recordId = $item['id']; - // no ned to add faceted index by id + // no need to add faceted index by id unset($item['id']); $searchIndex->addRecord($recordId, $item); } @@ -129,7 +129,7 @@ Using in application ```php setData($indexData); // create search instance $search = new Search($searchIndex); @@ -183,12 +183,12 @@ Note that RangeFilter is slow solution, it is better to avoid facets for highly ```php addIndexer('price', $rangeIndexer); @@ -210,21 +210,57 @@ $search->find($filters); RangeListIndexer allows you to use custom ranges list ```php )[1000] $index->addIndexer('price', $rangeIndexer); ``` Also, you can create your own indexers with range detection method + +### FixedArrayIndex + +FixedArrayIndex is much slower but requires significant less memory. +Working with an FixedArrayIndex is slightly different from ArrayIndex + +```php +writeMode(); +/* + * Getting products data from DB + * Sort data by $recordId before using Index->addRecord it can improve performance + */ +$data = [ + ['id'=>7, 'color'=>'black', 'price'=>100, 'sale'=>true, 'size'=>36], + ['id'=>9, 'color'=>'green', 'price'=>100, 'sale'=>true, 'size'=>40], + // .... +]; +foreach($data as $item){ + $recordId = $item['id']; + // no need to add faceted index by id + unset($item['id']); + $searchIndex->addRecord($recordId, $item); +} +// After the data is added, you need to commit the changes +$searchIndex->commitChanges(); +// save index data to some storage +$indexData = $searchIndex->getData(); +// We will use file for example +file_put_contents('./first-index.json', json_encode($indexData)); +``` + + ### More Examples * [Demo](./examples) * [Performance test](./tests/performance/readme.md) * [Bench](./tests/benchmark/readme.md) - ### Tested but discarded concepts **Bitmap** @@ -247,4 +283,5 @@ Also, you can create your own indexers with range detection method # Q&A * [Is it possible somehow to implement a full-text filter?](https://github.com/k-samuel/faceted-search/issues/3) -* [Would that be possible to use a DB as an index instead of a json file?](https://github.com/k-samuel/faceted-search/issues/5) \ No newline at end of file +* [Would that be possible to use a DB as an index instead of a json file?](https://github.com/k-samuel/faceted-search/issues/5) +* [Article about project history and base concepts of (in Russian)](https://habr.com/ru/post/595765/) \ No newline at end of file diff --git a/changelog.md b/changelog.md index db7ac5b..ead7117 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,34 @@ # Changelog +### v2.1.0 + +FixedArrayIndex + +FixedArrayIndex is much slower but requires significant less memory + +* FixedArrayIndex added +* KSamuel\FacetedSearch\Index is deprecated use KSamuel\FacetedSearch\Index\ArrayIndex instead +* Unit and performance tests for FixedArrayIndex +* Documentation updated + +PHPBench v2.1.0 ArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) + +| Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | +|----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| +| 10,000 | ~6Mb | ~0.0003 s. | ~0.002 s. | ~0.0001 s. | 907 | +| 50,000 | ~40Mb | ~0.001 s. | ~0.013 s. | ~0.0006 s. | 4550 | +| 100,000 | ~80Mb | ~0.003 s. | ~0.029 s. | ~0.001 s. | 8817 | +| 300,000 | ~189Mb | ~0.011 s. | ~0.108 s. | ~0.005 s. | 26891 | +| 1,000,000 | ~657Mb | ~0.052 s. | ~0.419 s. | ~0.018 s. | 90520 | + +PHPBench v2.1.0 FixedArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) + +| Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | +|----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| +| 10,000 | ~2Mb | ~0.0007 s. | ~0.006 s. | ~0.0002 s. | 907 | +| 50,000 | ~12Mb | ~0.003 s. | ~0.029 s. | ~0.001 s. | 4550 | +| 100,000 | ~23Mb | ~0.007 s. | ~0.058 s. | ~0.002 s. | 8817 | +| 300,000 | ~70Mb | ~0.021 s. | ~0.191 s. | ~0.008 s. | 26891 | +| 1,000,000 | ~233Mb | ~0.081 s. | ~0.702 s. | ~0.033 s. | 90520 | ### v2.0.3 (30.12.2021) Performance update diff --git a/composer.json b/composer.json index 6c9b383..bb47d60 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "k-samuel/faceted-search", - "version": "2.0.3", + "version": "2.1.0", "type": "library", "description": "PHP Faceted search", "keywords": ["php","faceted search"], diff --git a/src/Filter/RangeFilter.php b/src/Filter/RangeFilter.php index 2caccaa..5126579 100644 --- a/src/Filter/RangeFilter.php +++ b/src/Filter/RangeFilter.php @@ -62,7 +62,14 @@ public function filterResults(array $facetedData, ?array $inputIdKeys = null): a continue; } if (empty($limit)) { - $limit = $records; + /** + * @var array|\SplFixedArray $records + */ + if($records instanceof \SplFixedArray){ + $limit = $records->toArray(); + }else{ + $limit = $records; + } } else { // array sum (faster than array_merge here) foreach ($records as $item){ @@ -97,6 +104,9 @@ public function filterResults(array $facetedData, ?array $inputIdKeys = null): a $result = []; foreach ($start as $index => $exists) { + /** + * @var int $index + */ if (isset($compare[$index])) { $result[$index] = true; } diff --git a/src/Index.php b/src/Index.php index b16aeaf..039e27b 100644 --- a/src/Index.php +++ b/src/Index.php @@ -29,165 +29,14 @@ namespace KSamuel\FacetedSearch; -use KSamuel\FacetedSearch\Indexer\IndexerInterface; +use KSamuel\FacetedSearch\Index\ArrayIndex; /** * Simple faceted index * @package KSamuel\FacetedSearch + * @deprecated backward compatibility, use ArrayIndex */ -class Index +class Index extends ArrayIndex { - /** - * Index data - * @var array>> - */ - protected $data = []; - /** - * @var array - */ - protected $indexers = []; - /** - * @var null|array - */ - private $idMapCache = null; - /** - * Add record to index - * @param int $recordId - * @param array> $recordValues - ['fieldName'=>'fieldValue','fieldName2'=>['val1','val2']] - * @return bool - */ - public function addRecord(int $recordId, array $recordValues): bool - { - $this->resetLocalCache(); - foreach ($recordValues as $fieldName => $values) { - if (!is_array($values)) { - $values = [$values]; - } - - $values = array_unique($values); - - if (isset($this->indexers[$fieldName])) { - if (!isset($this->data[$fieldName])) { - $this->data[$fieldName] = []; - } - if (!$this->indexers[$fieldName]->add($this->data[$fieldName], $recordId, $values)) { - return false; - } - } else { - foreach ($values as $value) { - if (is_bool($value)) { - $value = (int)$value; - } - if(is_float($value)){ - $value = (string) $value; - } - $this->data[$fieldName][$value][] = $recordId; - } - } - } - return true; - } - - private function resetLocalCache(): void - { - $this->idMapCache = null; - } - - /** - * Get index data. Can be used for storing it to DB - * @return array>> - */ - public function getData(): array - { - return $this->data; - } - - /** - * Set index data. Can be used for restoring from DB - * @param array>> $data - */ - public function setData(array $data): void - { - $this->resetLocalCache(); - $this->data = $data; - } - - /** - * Get field data section from index - * @param string $fieldName - * @return array> - */ - public function getFieldData(string $fieldName): array - { - return $this->data[$fieldName] ?? []; - } - - /** - * Get all records from index - * @return array - */ - public function getAllRecordId(): array - { - return array_keys($this->getAllREcordIdMap()); - } - - /** - * Get all records from index as map [$id1=>true,...] - * @return array - */ - public function getAllRecordIdMap(): array - { - if ($this->idMapCache !== null) { - return $this->idMapCache; - } - - $result = []; - foreach ($this->data as $values) { - foreach ($values as $list) { - foreach ($list as $v) { - $result[$v] = true; - } - } - } - /** - * @var array $result - */ - - $this->idMapCache = $result; - return $result; - } - - /** - * Add specialized indexer for field - * @param string $fieldName - * @param IndexerInterface $indexer - */ - public function addIndexer(string $fieldName, IndexerInterface $indexer): void - { - $this->indexers[$fieldName] = $indexer; - } - - /** - * @param string $field - * @param mixed $value - * @return int - */ - public function getRecordsCount(string $field, $value) : int - { - if(!isset($this->data[$field][$value])){ - return 0; - } - return count($this->data[$field][$value]); - } - - /** - * Check if field exists - * @param string $fieldName - * @return bool - */ - public function hasField(string $fieldName) : bool - { - return isset($this->data[$fieldName]); - } } \ No newline at end of file diff --git a/src/Index/ArrayIndex.php b/src/Index/ArrayIndex.php new file mode 100644 index 0000000..2ac27c2 --- /dev/null +++ b/src/Index/ArrayIndex.php @@ -0,0 +1,193 @@ +>> + */ + protected $data = []; + /** + * @var array + */ + protected $indexers = []; + /** + * @var null|array + */ + private $idMapCache = null; + + /** + * Add record to index + * @param int $recordId + * @param array> $recordValues - ['fieldName'=>'fieldValue','fieldName2'=>['val1','val2']] + * @return bool + */ + public function addRecord(int $recordId, array $recordValues): bool + { + $this->resetLocalCache(); + foreach ($recordValues as $fieldName => $values) { + if (!is_array($values)) { + $values = [$values]; + } + + $values = array_unique($values); + + if (isset($this->indexers[$fieldName])) { + if (!isset($this->data[$fieldName])) { + $this->data[$fieldName] = []; + } + if (!$this->indexers[$fieldName]->add($this->data[$fieldName], $recordId, $values)) { + return false; + } + } else { + foreach ($values as $value) { + if (is_bool($value)) { + $value = (int)$value; + } + if(is_float($value)){ + $value = (string) $value; + } + $this->data[$fieldName][$value][] = $recordId; + } + } + } + return true; + } + + private function resetLocalCache(): void + { + $this->idMapCache = null; + } + + /** + * Get facet data. + * @return array>> + */ + public function getData(): array + { + return $this->data; + } + + /** + * Set index data. Can be used for restoring from DB + * @param array>> $data + */ + public function setData(array $data): void + { + $this->resetLocalCache(); + $this->data = $data; + } + + /** + * Get field data section from index + * @param string $fieldName + * @return array> + */ + public function getFieldData(string $fieldName): array + { + return $this->data[$fieldName] ?? []; + } + + /** + * Get all records from index + * @return array + */ + public function getAllRecordId(): array + { + return array_keys($this->getAllREcordIdMap()); + } + + /** + * Get all records from index as map [$id1=>true,...] + * @return array + */ + public function getAllRecordIdMap(): array + { + if ($this->idMapCache !== null) { + return $this->idMapCache; + } + + $result = []; + foreach ($this->data as $values) { + foreach ($values as $list) { + foreach ($list as $v) { + $result[$v] = true; + } + } + } + /** + * @var array $result + */ + + $this->idMapCache = $result; + return $result; + } + + /** + * Add specialized indexer for field + * @param string $fieldName + * @param IndexerInterface $indexer + */ + public function addIndexer(string $fieldName, IndexerInterface $indexer): void + { + $this->indexers[$fieldName] = $indexer; + } + + /** + * @param string $field + * @param mixed $value + * @return int + */ + public function getRecordsCount(string $field, $value) : int + { + if(!isset($this->data[$field][$value])){ + return 0; + } + return count($this->data[$field][$value]); + } + + /** + * Check if field exists + * @param string $fieldName + * @return bool + */ + public function hasField(string $fieldName) : bool + { + return isset($this->data[$fieldName]); + } +} \ No newline at end of file diff --git a/src/Index/FixedArrayIndex.php b/src/Index/FixedArrayIndex.php new file mode 100644 index 0000000..62b609c --- /dev/null +++ b/src/Index/FixedArrayIndex.php @@ -0,0 +1,252 @@ +|\SplFixedArray>> + */ + protected $data = []; + /** + * @var null|array + */ + private $idMapCache = null; + /** + * @var array + */ + protected $indexers = []; + /** + * @var bool + */ + protected $isCompact = false; + + /** + * Add record to index + * @param int $recordId + * @param array> $recordValues - ['fieldName'=>'fieldValue','fieldName2'=>['val1','val2']] + * @return bool + */ + public function addRecord(int $recordId, array $recordValues): bool + { + if($this->isCompact){ + throw new \RuntimeException('Cannot add record in readonly mode. Use $index->writeMode()'); + } + + $this->resetLocalCache(); + + foreach ($recordValues as $fieldName => $values) { + if (!is_array($values)) { + $values = [$values]; + } + + $values = array_unique($values); + + if (isset($this->indexers[$fieldName])) { + if (!isset($this->data[$fieldName])) { + $this->data[$fieldName] = []; + } + if (!$this->indexers[$fieldName]->add($this->data[$fieldName], $recordId, $values)) { + return false; + } + } else { + foreach ($values as $value) { + if (is_bool($value)) { + $value = (int)$value; + } + if (is_float($value)) { + $value = (string)$value; + } + $this->data[$fieldName][$value][] = $recordId; + } + } + } + return true; + } + + /** + * Get facet data. + * @return array|\SplFixedArray>> + */ + public function getData(): array + { + return $this->data; + } + + private function resetLocalCache(): void + { + $this->idMapCache = null; + } + /** + * Get index data. Can be used for storing it to DB + * @return array>> + */ + public function export(): array + { + if ($this->isCompact) { + $this->writeMode(); + } + /** + * @var array>> + */ + return $this->data; + } + + /** + * Enable write mode, don't forget to commit changes + */ + public function writeMode(): void + { + foreach ($this->data as &$value) { + /** + * @var \SplFixedArray $recordList + */ + foreach ($value as &$recordList) { + if($recordList instanceof \SplFixedArray){ + $recordList = $recordList->toArray(); + } + } + } + $this->isCompact = false; + } + + public function commitChanges() : void + { + foreach ($this->data as &$value) { + /** + * @var array|\SplFixedArray $recordList + */ + foreach ($value as &$recordList) { + if(is_array($recordList)){ + $recordList = \SplFixedArray::fromArray($recordList); + } + } + } + $this->isCompact = true; + } + + /** + * Set index data. Can be used for restoring from DB + * @param array>> $data + */ + public function setData(array $data): void + { + $this->isCompact = false; + $this->data = $data; + $this->resetLocalCache(); + $this->commitChanges(); + } + + /** + * Get field data section from index + * @param string $fieldName + * @return array|\SplFixedArray> + */ + public function getFieldData(string $fieldName): array + { + return $this->data[$fieldName] ?? []; + } + + + /** + * Get all records from index + * @return array + */ + public function getAllRecordId(): array + { + return array_keys($this->getAllREcordIdMap()); + } + + /** + * Get all records from index as map [$id1=>true,...] + * @return array + */ + public function getAllRecordIdMap(): array + { + if ($this->idMapCache !== null) { + return $this->idMapCache; + } + + $result = []; + foreach ($this->data as $values) { + foreach ($values as $list) { + foreach ($list as $v) { + $result[$v] = true; + } + } + } + /** + * @var array $result + */ + + $this->idMapCache = $result; + return $result; + } + + /** + * Add specialized indexer for field + * @param string $fieldName + * @param IndexerInterface $indexer + */ + public function addIndexer(string $fieldName, IndexerInterface $indexer): void + { + $this->indexers[$fieldName] = $indexer; + } + + /** + * @param string $field + * @param mixed $value + * @return int + */ + public function getRecordsCount(string $field, $value): int + { + if (!isset($this->data[$field][$value])) { + return 0; + } + return count($this->data[$field][$value]); + } + + /** + * Check if field exists + * @param string $fieldName + * @return bool + */ + public function hasField(string $fieldName): bool + { + return isset($this->data[$fieldName]); + } +} \ No newline at end of file diff --git a/src/Index/IndexInterface.php b/src/Index/IndexInterface.php new file mode 100644 index 0000000..c189d1e --- /dev/null +++ b/src/Index/IndexInterface.php @@ -0,0 +1,96 @@ +> $recordValues - ['fieldName'=>'fieldValue','fieldName2'=>['val1','val2']] + * @return bool + */ + public function addRecord(int $recordId, array $recordValues): bool; + + + /** + * Get field data section from index + * @param string $fieldName + * @return array> + */ + public function getFieldData(string $fieldName): array; + + + /** + * Get all records from index + * @return array + */ + public function getAllRecordId(): array; + + /** + * Get all records from index as map [$id1=>true,...] + * @return array + */ + public function getAllRecordIdMap(): array; + + /** + * Add specialized indexer for field + * @param string $fieldName + * @param IndexerInterface $indexer + */ + public function addIndexer(string $fieldName, IndexerInterface $indexer): void; + + /** + * @param string $field + * @param mixed $value + * @return int + */ + public function getRecordsCount(string $field, $value) : int; + + /** + * Check if field exists + * @param string $fieldName + * @return bool + */ + public function hasField(string $fieldName) : bool; + + /** + * Get facet data. + * @return array|\SplFixedArray>> + */ + public function getData(): array; +} \ No newline at end of file diff --git a/src/Search.php b/src/Search.php index dfba634..c68f960 100644 --- a/src/Search.php +++ b/src/Search.php @@ -31,6 +31,7 @@ use KSamuel\FacetedSearch\Filter\FilterInterface; use KSamuel\FacetedSearch\Filter\ValueFilter; +use KSamuel\FacetedSearch\Index\IndexInterface; /** * Class Search @@ -40,15 +41,15 @@ class Search { /** - * @var Index + * @var IndexInterface */ protected $index; /** * Search constructor. - * @param Index $index + * @param IndexInterface $index */ - public function __construct(Index $index) + public function __construct(IndexInterface $index) { $this->index = $index; } @@ -206,11 +207,11 @@ private function findFilters(array $filters = [], array $inputRecords = [], bool } /** - * @param array $a + * @param array|\SplFixedArray $a * @param array $b * @return int */ - private function getIntersectIntMapCount(array $a, array $b): int + private function getIntersectIntMapCount($a, array $b): int { $intersectLen = 0; foreach ($a as $key) { diff --git a/src/Sorter/ByField.php b/src/Sorter/ByField.php index 0d4bdd6..ea1186c 100644 --- a/src/Sorter/ByField.php +++ b/src/Sorter/ByField.php @@ -37,11 +37,11 @@ class ByField public const SORT_DESC = 1; /** - * @var Index + * @var Index\IndexInterface */ private $index; - public function __construct(Index $index) + public function __construct(Index\IndexInterface $index) { $this->index = $index; } @@ -96,14 +96,17 @@ public function sort( } /** - * @param array $a + * @param array|\SplFixedArray $a * @param array $b * @return array */ - private function intersectIntMap(array $a, array $b): array + private function intersectIntMap($a, array $b): array { $result = []; foreach ($a as $key) { + /** + * @var int $key + */ if (isset($b[$key])) { $result[] = $key; } diff --git a/tests/benchmark/SearchBench.php b/tests/benchmark/ArrayIndexBench.php similarity index 92% rename from tests/benchmark/SearchBench.php rename to tests/benchmark/ArrayIndexBench.php index 59846c7..d28c481 100644 --- a/tests/benchmark/SearchBench.php +++ b/tests/benchmark/ArrayIndexBench.php @@ -21,16 +21,16 @@ * @Revs(10) * @BeforeMethods({"before"}) */ -class SearchBench +class ArrayIndexBench { /** * @var Index */ - private $index; + protected $index; /** * @var Search */ - private $search; + protected $search; /** * @var int */ @@ -38,15 +38,15 @@ class SearchBench /** * @var FilterInterface[] */ - private $filters; + protected $filters; /** * @var array */ - private $firstResults; + protected $firstResults; /** * @var ByField */ - private $sorter; + protected $sorter; public function before(): void { diff --git a/tests/benchmark/Bench/S100kBench.php b/tests/benchmark/Bench/ArrayIndex/S100kBench.php similarity index 77% rename from tests/benchmark/Bench/S100kBench.php rename to tests/benchmark/Bench/ArrayIndex/S100kBench.php index 8ae7656..8960a31 100644 --- a/tests/benchmark/Bench/S100kBench.php +++ b/tests/benchmark/Bench/ArrayIndex/S100kBench.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench; +namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench\ArrayIndex; use KSamuel\FacetedSearch\Filter\FilterInterface; use KSamuel\FacetedSearch\Index; use KSamuel\FacetedSearch\Search; use KSamuel\FacetedSearch\Filter\ValueFilter; use KSamuel\FacetedSearch\Tests\Benchmark\DatasetFactory; -use KSamuel\FacetedSearch\Tests\Benchmark\SearchBench; +use KSamuel\FacetedSearch\Tests\Benchmark\ArrayIndexBench; use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; use PhpBench\Benchmark\Metadata\Annotations\Iterations; use PhpBench\Benchmark\Metadata\Annotations\Revs; @@ -19,7 +19,7 @@ * @Revs(10) * @BeforeMethods({"before"}) */ -class S100KBench extends SearchBench +class S100KBench extends ArrayIndexBench { /** * @var int diff --git a/tests/benchmark/Bench/S10kBench.php b/tests/benchmark/Bench/ArrayIndex/S10kBench.php similarity index 77% rename from tests/benchmark/Bench/S10kBench.php rename to tests/benchmark/Bench/ArrayIndex/S10kBench.php index 8c77206..72a7995 100644 --- a/tests/benchmark/Bench/S10kBench.php +++ b/tests/benchmark/Bench/ArrayIndex/S10kBench.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench; +namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench\ArrayIndex; use KSamuel\FacetedSearch\Filter\FilterInterface; use KSamuel\FacetedSearch\Index; use KSamuel\FacetedSearch\Search; use KSamuel\FacetedSearch\Filter\ValueFilter; use KSamuel\FacetedSearch\Tests\Benchmark\DatasetFactory; -use KSamuel\FacetedSearch\Tests\Benchmark\SearchBench; +use KSamuel\FacetedSearch\Tests\Benchmark\ArrayIndexBench; use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; use PhpBench\Benchmark\Metadata\Annotations\Iterations; use PhpBench\Benchmark\Metadata\Annotations\Revs; @@ -19,7 +19,7 @@ * @Revs(10) * @BeforeMethods({"before"}) */ -class S10KBench extends SearchBench +class S10kBench extends ArrayIndexBench { /** * @var int diff --git a/tests/benchmark/Bench/S1mBench.php b/tests/benchmark/Bench/ArrayIndex/S1mBench.php similarity index 77% rename from tests/benchmark/Bench/S1mBench.php rename to tests/benchmark/Bench/ArrayIndex/S1mBench.php index 468a09a..58041f4 100644 --- a/tests/benchmark/Bench/S1mBench.php +++ b/tests/benchmark/Bench/ArrayIndex/S1mBench.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench; +namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench\ArrayIndex; use KSamuel\FacetedSearch\Filter\FilterInterface; use KSamuel\FacetedSearch\Index; use KSamuel\FacetedSearch\Search; use KSamuel\FacetedSearch\Filter\ValueFilter; use KSamuel\FacetedSearch\Tests\Benchmark\DatasetFactory; -use KSamuel\FacetedSearch\Tests\Benchmark\SearchBench; +use KSamuel\FacetedSearch\Tests\Benchmark\ArrayIndexBench; use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; use PhpBench\Benchmark\Metadata\Annotations\Iterations; use PhpBench\Benchmark\Metadata\Annotations\Revs; @@ -19,7 +19,7 @@ * @Revs(10) * @BeforeMethods({"before"}) */ -class S1MBench extends SearchBench +class S1mBench extends ArrayIndexBench { /** * @var int diff --git a/tests/benchmark/Bench/S300kBench.php b/tests/benchmark/Bench/ArrayIndex/S300kBench.php similarity index 77% rename from tests/benchmark/Bench/S300kBench.php rename to tests/benchmark/Bench/ArrayIndex/S300kBench.php index 7c1a6c3..a890af4 100644 --- a/tests/benchmark/Bench/S300kBench.php +++ b/tests/benchmark/Bench/ArrayIndex/S300kBench.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench; +namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench\ArrayIndex; use KSamuel\FacetedSearch\Filter\FilterInterface; use KSamuel\FacetedSearch\Index; use KSamuel\FacetedSearch\Search; use KSamuel\FacetedSearch\Filter\ValueFilter; use KSamuel\FacetedSearch\Tests\Benchmark\DatasetFactory; -use KSamuel\FacetedSearch\Tests\Benchmark\SearchBench; +use KSamuel\FacetedSearch\Tests\Benchmark\ArrayIndexBench; use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; use PhpBench\Benchmark\Metadata\Annotations\Iterations; use PhpBench\Benchmark\Metadata\Annotations\Revs; @@ -19,7 +19,7 @@ * @Revs(10) * @BeforeMethods({"before"}) */ -class S300KBench extends SearchBench +class S300kBench extends ArrayIndexBench { /** * @var int diff --git a/tests/benchmark/Bench/S50kBench.php b/tests/benchmark/Bench/ArrayIndex/S50kBench.php similarity index 77% rename from tests/benchmark/Bench/S50kBench.php rename to tests/benchmark/Bench/ArrayIndex/S50kBench.php index a242640..c1ac391 100644 --- a/tests/benchmark/Bench/S50kBench.php +++ b/tests/benchmark/Bench/ArrayIndex/S50kBench.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench; +namespace KSamuel\FacetedSearch\Tests\Benchmark\Bench\ArrayIndex; use KSamuel\FacetedSearch\Filter\FilterInterface; use KSamuel\FacetedSearch\Index; use KSamuel\FacetedSearch\Search; use KSamuel\FacetedSearch\Filter\ValueFilter; use KSamuel\FacetedSearch\Tests\Benchmark\DatasetFactory; -use KSamuel\FacetedSearch\Tests\Benchmark\SearchBench; +use KSamuel\FacetedSearch\Tests\Benchmark\ArrayIndexBench; use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; use PhpBench\Benchmark\Metadata\Annotations\Iterations; use PhpBench\Benchmark\Metadata\Annotations\Revs; @@ -19,7 +19,7 @@ * @Revs(10) * @BeforeMethods({"before"}) */ -class S50KBench extends SearchBench +class S50kBench extends ArrayIndexBench { /** * @var int diff --git a/tests/benchmark/Bench/FixedArrayIndex/S100kBench.php b/tests/benchmark/Bench/FixedArrayIndex/S100kBench.php new file mode 100644 index 0000000..1d799b2 --- /dev/null +++ b/tests/benchmark/Bench/FixedArrayIndex/S100kBench.php @@ -0,0 +1,29 @@ +dataDir . $size . '/data.json'; if (!file_exists($dataFile)) { $this->createDataset($size, $dataFile); } + $index = new Index\ArrayIndex(); + $this->loadData($index, $dataFile); + return $index; + } - return $this->loadData($dataFile); + /** + * @param int $size + */ + public function getFixedFacetedIndex(int $size): Index\FixedArrayIndex + { + $dataFile = $this->dataDir . $size . '/data.json'; + if (!file_exists($dataFile)) { + $this->createDataset($size, $dataFile); + } + $index = new Index\FixedArrayIndex(); + $index->writeMode(); + $this->loadData($index, $dataFile); + $index->commitChanges(); + return $index; } + /** * @param int $size * @param string $file @@ -107,9 +125,8 @@ private function createDataset(int $size, string $file): void fclose($f); } - private function loadData(string $file): Index + private function loadData(Index\IndexInterface $index, string $file) : void { - $index = new Index(); $f = fopen($file, 'r'); if (empty($f)) { throw new \RuntimeException('Cannot open file ' . $file); @@ -118,11 +135,10 @@ private function loadData(string $file): Index if (empty($line)) { continue; } - $row = json_decode($line, true); + $row = \json_decode($line, true); $id = $row['id']; unset($row['id']); - $index->addRecord($id, $row); + $index->addRecord((int)$id, $row); } - return $index; } } \ No newline at end of file diff --git a/tests/benchmark/FixedArrayIndexBench.php b/tests/benchmark/FixedArrayIndexBench.php new file mode 100644 index 0000000..9f9e128 --- /dev/null +++ b/tests/benchmark/FixedArrayIndexBench.php @@ -0,0 +1,40 @@ +getFixedFacetedIndex($this->dataSize); + $this->search = new Search($index); + $this->filters = [ + new ValueFilter('color', 'black'), + new ValueFilter('warehouse', [789, 45, 65, 1, 10]), + new ValueFilter('type', ["normal", "middle"]) + ]; + $this->firstResults = $this->search->find($this->filters); + $this->sorter = new ByField($index); + } +} \ No newline at end of file diff --git a/tests/performance/find.php b/tests/performance/find.php index 2a6810e..53838a1 100644 --- a/tests/performance/find.php +++ b/tests/performance/find.php @@ -14,7 +14,7 @@ $time = (microtime(true) - $t); $memUse = (int)((memory_get_usage() - $m) / 1024 / 1024); -$index = new Index(); +$index = new Index\ArrayIndex(); $index->setData($indexData); $resultData[] = ['Index memory usage', (string) $memUse. "Mb",'']; $resultData[] = ['Loading time', number_format($time,6) . 's', '']; @@ -54,14 +54,6 @@ $time = microtime(true) - $t; $resultData[] = ['Sort by quantity DESC', number_format($time,6) . "s", count($results)]; -// uncoment to verify results -/* -$data = \json_decode(file_get_contents('./data.json'), true); -foreach ($results as $id) { - echo $id . ' : ' . $data[$id][$sortField] . PHP_EOL; -} -*/ - //test acceptable $t = microtime(true); $filtersData = $search->findAcceptableFilters($filters); diff --git a/tests/performance/findFixed.php b/tests/performance/findFixed.php new file mode 100644 index 0000000..543d9e0 --- /dev/null +++ b/tests/performance/findFixed.php @@ -0,0 +1,90 @@ +setData($indexData); +$time = (microtime(true) - $t); +unset($indexData); +gc_collect_cycles(); +$memUse = (int)((memory_get_usage() - $m) / 1024 / 1024); + + + +$time = (microtime(true) - $t); +gc_collect_cycles(); +$memUse = (int)((memory_get_usage() - $m) / 1024 / 1024); + +$resultData[] = ['Index memory usage', (string) $memUse. "Mb",'']; +$resultData[] = ['Loading time', number_format($time,6) . 's', '']; + +$search = new Search($index); + +$filters = [ + new ValueFilter('color', 'black'), + new ValueFilter('warehouse', [789, 45, 65, 1, 10]), + new ValueFilter('type', ["normal", "middle"]), +]; + +$filters2 = [ + new ValueFilter('color', 'black'), + new ValueFilter('warehouse', [789, 45, 65, 1, 10]), + new RangeFilter('price', ['min' => 1000, 'max' => 5000]) +]; + + +/// test find +$t = microtime(true); +$results = $search->find($filters); +$time = microtime(true) - $t; +$resultData[] = ['Find Results', number_format($time,6) . "s", count($results)]; + +/// test find with Range +$t = microtime(true); +$results2 = $search->find($filters2); +$time = microtime(true) - $t; +$resultData[] = ['Find Results (ranges)', number_format($time,6) . "s", count($results2)]; + +// test sort +$sorter = new ByField($index); +$sortField = 'quantity'; +$t = microtime(true); +$results = $sorter->sort($results, $sortField, ByField::SORT_DESC); +$time = microtime(true) - $t; +$resultData[] = ['Sort by quantity DESC', number_format($time,6) . "s", count($results)]; + +//test acceptable +$t = microtime(true); +$filtersData = $search->findAcceptableFilters($filters); +$time = microtime(true) - $t; +$resultData[] = ['Filters', number_format($time,6) . "s", count($filters)]; + +//test acceptable with count +$t = microtime(true); +$filtersData = $search->findAcceptableFiltersCount($filters); +$time = microtime(true) - $t; +$resultData[] = ['Filters with count', number_format($time,6) . "s", count($filters)]; + + +$colLen = [25, 14, 10]; +echo str_repeat("-", 56) . PHP_EOL; +foreach ($resultData as $index => $cols) { + if ($index === 2) { + echo str_repeat("-", 56) . PHP_EOL; + } + + foreach ($cols as $id => $item) { + echo '| ' . str_pad(' ' . $item, $colLen[$id]); + } + echo "|" . PHP_EOL; +} +echo str_repeat("-", 56) . PHP_EOL; diff --git a/tests/unit/IndexTest.php b/tests/unit/IndexTest.php index 93409c9..587d707 100644 --- a/tests/unit/IndexTest.php +++ b/tests/unit/IndexTest.php @@ -9,7 +9,7 @@ class IndexTest extends TestCase { public function testAddRecord() { - $index = new Index(); + $index = new Index\ArrayIndex(); $this->assertTrue($index->addRecord(112, ['vendor'=>'Tester','price' => 100])); $this->assertTrue($index->addRecord(113, ['vendor'=>'Tester2','price' => 101])); $this->assertTrue($index->addRecord(114, ['vendor'=>'Tester2','price' => 101])); @@ -25,4 +25,23 @@ public function testAddRecord() ], $index->getData()); $this->assertTrue($index->addRecord(114, ['vendor'=>'Tester2','price' => 0.15])); } + + public function testFixedAddRecord() + { + $index = new Index\FixedArrayIndex(); + $this->assertTrue($index->addRecord(112, ['vendor'=>'Tester','price' => 100])); + $this->assertTrue($index->addRecord(113, ['vendor'=>'Tester2','price' => 101])); + $this->assertTrue($index->addRecord(114, ['vendor'=>'Tester2','price' => 101])); + $this->assertEquals([ + 'vendor' => [ + 'Tester' => [112], + 'Tester2' => [113,114] + ], + 'price' => [ + 100 => [112], + 101 => [113,114] + ] + ], $index->export()); + $this->assertTrue($index->addRecord(114, ['vendor'=>'Tester2','price' => 0.15])); + } } \ No newline at end of file diff --git a/tests/unit/Indexer/Number/RangeIndexerTest.php b/tests/unit/Indexer/Number/RangeIndexerTest.php index d809bb2..2d296e7 100644 --- a/tests/unit/Indexer/Number/RangeIndexerTest.php +++ b/tests/unit/Indexer/Number/RangeIndexerTest.php @@ -6,7 +6,7 @@ class RangeIndexerTest extends TestCase { public function testAddRecord() { - $index = new Index(); + $index = new Index\ArrayIndex(); $indexer = new \KSamuel\FacetedSearch\Indexer\Number\RangeIndexer(100); $index->addIndexer('price', $indexer); @@ -23,4 +23,24 @@ public function testAddRecord() ] ], $index->getData()); } + + public function testFixedAddRecord() + { + $index = new Index\FixedArrayIndex(); + $indexer = new \KSamuel\FacetedSearch\Indexer\Number\RangeIndexer(100); + $index->addIndexer('price', $indexer); + + $this->assertTrue($index->addRecord(2, ['price' => 90])); + $this->assertTrue($index->addRecord(3, ['price' => 100])); + $this->assertTrue($index->addRecord(4, ['price' => 110])); + $this->assertTrue($index->addRecord(5, ['price' => 1000])); + + $this->assertEquals([ + 'price' => [ + 0 => [2], + 100 => [3 , 4], + 1000 => [5] + ] + ], $index->export()); + } } \ No newline at end of file diff --git a/tests/unit/Indexer/Number/RangeListIndexerTest.php b/tests/unit/Indexer/Number/RangeListIndexerTest.php index db434b6..742eaf5 100644 --- a/tests/unit/Indexer/Number/RangeListIndexerTest.php +++ b/tests/unit/Indexer/Number/RangeListIndexerTest.php @@ -7,7 +7,7 @@ class RangeListIndexerTest extends TestCase { public function testAddRecord() { - $index = new Index(); + $index = new Index\ArrayIndex(); $indexer = new RangeListIndexer([ 100,200,150,500 ]); @@ -26,4 +26,24 @@ public function testAddRecord() ] ], $index->getData()); } + + public function testFixedAddRecord() + { + $index = new Index\FixedArrayIndex(); + $indexer = new RangeListIndexer([100,200,150,500]); + $index->addIndexer('price', $indexer); + + $this->assertTrue($index->addRecord(2, ['price' => 90])); + $this->assertTrue($index->addRecord(3, ['price' => 100])); + $this->assertTrue($index->addRecord(4, ['price' => 110])); + $this->assertTrue($index->addRecord(5, ['price' => 1000])); + + $this->assertEquals([ + 'price' => [ + 0 => [2], + 100 => [3,4], + 500 => [5] + ] + ], $index->getData()); + } } \ No newline at end of file diff --git a/tests/unit/SearchTest.php b/tests/unit/SearchArrayIndexTest.php similarity index 99% rename from tests/unit/SearchTest.php rename to tests/unit/SearchArrayIndexTest.php index 993ea3b..2243f5e 100644 --- a/tests/unit/SearchTest.php +++ b/tests/unit/SearchArrayIndexTest.php @@ -6,7 +6,7 @@ use KSamuel\FacetedSearch\Filter\RangeFilter; use KSamuel\FacetedSearch\Index; -class SearchTest extends TestCase +class SearchArrayIndexTest extends TestCase { public function testFind() diff --git a/tests/unit/SearchFixedArrayIndexTest.php b/tests/unit/SearchFixedArrayIndexTest.php new file mode 100644 index 0000000..81cd3a1 --- /dev/null +++ b/tests/unit/SearchFixedArrayIndexTest.php @@ -0,0 +1,353 @@ +writeMode(); + foreach ($records as $id => $item) { + if(isset($item['id'])){ + $id = $item['id']; + unset($item['id']); + } + $index->addRecord($id, $item); + } + $index->commitChanges(); + return $index; + } + + public function testFind() + { + $records = $this->getTestData(); + $index = $this->loadIndex($records); + + $facets = new Search($index); + $filter = new ValueFilter('vendor'); + $filter->setValue(['Samsung', 'Apple']); + + $filter2 = new RangeFilter('cam_mp'); + $filter2->setValue(['min' => 16]); + + $filter3 = new RangeFilter('price'); + $filter3->setValue(['max' => 80000]); + + $filter4 = new ValueFilter('sale'); + $filter4->setValue(true); + + $filters = [ + $filter, + $filter2, + $filter3, + $filter4 + ]; + $result = $facets->find($filters); + sort($result); + $this->assertEquals([3, 4], $result); + + $result = $facets->find($filters, array_keys($records)); + sort($result); + $this->assertEquals([3, 4], $result); + + $filter = new ValueFilter('vendor'); + $filter->setValue(['Google']); + $result = $facets->find([$filter]); + $this->assertEquals([], $result); + + $index->setData($index->getData()); + $filter = new ValueFilter('vendor_field'); + $filter->setValue(['Google']); + $result = $facets->find([$filter], [3, 4]); + $this->assertEquals([], $result); + } + + public function testFindWithLimit() + { + $records = $this->getTestData(); + $index = $this->loadIndex($records); + + $facets = new Search($index); + $filter = new ValueFilter('vendor'); + $filter->setValue(['Samsung', 'Apple']); + $result = $facets->find([$filter], [1, 3]); + $result = array_flip($result); + $this->assertArrayHasKey(1, $result); + $this->assertArrayHasKey(3, $result); + } + + public function testGetAcceptableFilters() + { + $records = $this->getTestData(); + $index = $this->loadIndex($records); + + $facets = new Search($index); + $filter = new ValueFilter('color', 'black'); + + $acceptableFilters = $facets->findAcceptableFilters([$filter]); + + $expect = [ + 'vendor' => ['Apple', 'Samsung', 'Xiaomi'], + 'model' => ['Iphone X Pro Max', 'Galaxy S20', 'Galaxy A5', 'MI 9'], + 'price' => [80999, 70599, 15000, 26000], + 'color' => ['black', 'white', 'yellow'], + 'has_phones' => [1], + 'cam_mp' => [40, 105, 12, 48], + 'sale' => [1, 0] + ]; + foreach ($expect as $field => &$values) { + sort($values); + } + unset($values); + foreach ($acceptableFilters as $field => &$values) { + sort($values); + } + unset($values); + + foreach ($expect as $filter => $values) { + $this->assertArrayHasKey($filter, $acceptableFilters); + $this->assertEquals($values, $acceptableFilters[$filter]); + } + } + + public function testGetAcceptableFiltersCountNoFilters() + { + $records = [ + ['color' => 'black', 'size' => 7, 'group' => 'A'], + ['color' => 'black', 'size' => 8, 'group' => 'A'], + ['color' => 'white', 'size' => 7, 'group' => 'B'], + ['color' => 'yellow', 'size' => 7, 'group' => 'C'], + ['color' => 'black', 'size' => 7, 'group' => 'C'], + ]; + $index = $this->loadIndex($records); + $facets = new Search($index); + + $acceptableFilters = $facets->findAcceptableFiltersCount(); + + $expect = [ + 'color' => ['black' => 3, 'white' => 1, 'yellow' => 1], + 'size' => [7 => 4, 8 => 1], + 'group' => ['A' => 2, 'B' => 1, 'C' => 2], + ]; + foreach ($expect as $field => &$values) { + asort($values); + } + unset($values); + foreach ($acceptableFilters as $field => &$values) { + asort($values); + } + unset($values); + + foreach ($expect as $filter => $values) { + $this->assertArrayHasKey($filter, $acceptableFilters); + $this->assertEquals($values, $acceptableFilters[$filter]); + } + } + + public function testGetAcceptableFiltersCountLimit() + { + $records = [ + ['id' => 1, 'color' => 'black', 'size' => 7, 'group' => 'A'], + ['id' => 2, 'color' => 'black', 'size' => 8, 'group' => 'A'], + ['id' => 3, 'color' => 'white', 'size' => 7, 'group' => 'B'], + ['id' => 4, 'color' => 'yellow', 'size' => 7, 'group' => 'C'], + ['id' => 5, 'color' => 'black', 'size' => 7, 'group' => 'C'], + ]; + $index = $this->loadIndex($records); + $facets = new Search($index); + + $acceptableFilters = $facets->findAcceptableFiltersCount([], [1, 2]); + + $expect = [ + 'color' => ['black' => 2], + 'size' => [7 => 1, 8 => 1], + 'group' => ['A' => 2], + ]; + foreach ($expect as $field => &$values) { + asort($values); + } + unset($values); + foreach ($acceptableFilters as $field => &$values) { + asort($values); + } + unset($values); + + foreach ($expect as $filter => $values) { + $this->assertArrayHasKey($filter, $acceptableFilters); + $this->assertEquals($values, $acceptableFilters[$filter]); + } + } + + public function testGetAcceptableFiltersCount() + { + $records = $this->getTestData(); + $index = $this->loadIndex($records); + $facets = new Search($index); + $filter = new ValueFilter('color', 'black'); + + $acceptableFilters = $facets->findAcceptableFiltersCount([$filter]); + + $expect = [ + 'vendor' => ['Apple' => 1, 'Samsung' => 2, 'Xiaomi' => 1], + 'model' => ['Iphone X Pro Max' => 1, 'Galaxy S20' => 1, 'Galaxy A5' => 1, 'MI 9' => 1], + 'price' => [80999 => 1, 70599 => 1, 15000 => 1, 26000 => 1], + // self filtering is not using by facets logic + 'color' => ['black' => 4, 'white' => 1, 'yellow' => 1], + 'has_phones' => [1 => 4], + 'cam_mp' => [40 => 1, 105 => 1, 12 => 1, 48 => 1], + 'sale' => [1 => 3, 0 => 1] + ]; + foreach ($expect as $field => &$values) { + asort($values); + } + unset($values); + foreach ($acceptableFilters as $field => &$values) { + asort($values); + } + unset($values); + + foreach ($expect as $filter => $values) { + $this->assertArrayHasKey($filter, $acceptableFilters); + $this->assertEquals($values, $acceptableFilters[$filter]); + } + } + + public function testGetAcceptableFiltersCountMulty() + { + $records = [ + ['color' => 'black', 'size' => 7, 'group' => 'A'], + ['color' => 'black', 'size' => 8, 'group' => 'A'], + ['color' => 'white', 'size' => 7, 'group' => 'B'], + ['color' => 'yellow', 'size' => 7, 'group' => 'C'], + ['color' => 'black', 'size' => 7, 'group' => 'C'], + ]; + $index = $this->loadIndex($records); + $facets = new Search($index); + $filter = new ValueFilter('color', 'black'); + $filter2 = new ValueFilter('size', 7); + + $acceptableFilters = $facets->findAcceptableFiltersCount([$filter, $filter2]); + + $expect = [ + 'color' => ['black' => 2, 'white' => 1, 'yellow' => 1], + 'size' => [7 => 2, 8 => 1], + 'group' => ['A' => 1, 'C' => 1], + ]; + foreach ($expect as $field => &$values) { + asort($values); + } + unset($values); + foreach ($acceptableFilters as $field => &$values) { + asort($values); + } + unset($values); + + foreach ($expect as $filter => $values) { + $this->assertArrayHasKey($filter, $acceptableFilters); + $this->assertEquals($values, $acceptableFilters[$filter]); + } + } + + public function testFindFloat() + { + $records = [ + ['id'=>1, 'color' => 'black', 'size' => 7.5, 'group' => 'A'], + ['id'=>2, 'color' => 'black', 'size' => 8.9, 'group' => 'A'], + ['id'=>3, 'color' => 'white', 'size' => 7.11, 'group' => 'B'], + ]; + $index = $this->loadIndex($records); + + $facets = new Search($index); + $filter = new ValueFilter('size', 7.11); + $result = $facets->find([$filter]); + $this->assertEquals(3,$result[0]); + $filter = new ValueFilter('size', '8.9'); + $result = $facets->find([$filter]); + $this->assertEquals(2, $result[0]); + $acceptableFilters = $facets->findAcceptableFiltersCount([$filter]); + + $expect = [ + 'color' => ['black' =>1], + 'size' => ['8.9' => 1,'7.5'=>1,'7.11'=>1], + 'group' => ['A' =>1] + ]; + foreach ($expect as &$values) { + asort($values); + } + unset($values); + foreach ($acceptableFilters as &$values) { + asort($values); + } + unset($values); + + foreach ($expect as $filter => $values) { + $this->assertArrayHasKey($filter, $acceptableFilters); + $this->assertEquals($values, $acceptableFilters[$filter]); + } + } + + public function getTestData(): array + { + return [ + 1 => [ + 'vendor' => 'Apple', + 'model' => 'Iphone X', + 'price' => 80999, + 'color' => 'white', + 'has_phones' => false, + 'cam_mp' => 20, + 'sale' => true, + ], + 2 => [ + 'vendor' => 'Apple', + 'model' => 'Iphone X Pro Max', + 'price' => 80999, + 'color' => 'black', + 'has_phones' => true, + 'cam_mp' => 40, + 'sale' => true, + ], + 3 => [ + 'vendor' => 'Samsung', + 'model' => 'Galaxy S20', + 'price' => 70599, + 'color' => 'yellow', + 'has_phones' => true, + 'cam_mp' => 105, + 'sale' => true, + ], + 4 => [ + 'vendor' => 'Samsung', + 'model' => 'Galaxy S20', + 'price' => 70599, + 'color' => 'black', + 'has_phones' => true, + 'cam_mp' => 105, + 'sale' => true, + ], + 5 => [ + 'vendor' => 'Samsung', + 'model' => 'Galaxy A5', + 'price' => 15000, + 'color' => 'black', + 'has_phones' => true, + 'cam_mp' => 12, + 'sale' => true, + ], + 6 => [ + 'vendor' => 'Xiaomi', + 'model' => 'MI 9', + 'price' => 26000, + 'color' => 'black', + 'has_phones' => true, + 'cam_mp' => 48, + 'sale' => false, + ] + ]; + } +} \ No newline at end of file diff --git a/tests/unit/Sorter/FieldTest.php b/tests/unit/Sorter/FieldTest.php index 3441407..561376f 100644 --- a/tests/unit/Sorter/FieldTest.php +++ b/tests/unit/Sorter/FieldTest.php @@ -18,14 +18,24 @@ public function testSortDesc() 4 => ['size' => 8, 'tag' => 1], 5 => ['size' => 8, 'tag' => 2] ]; - $index = new Index(); + $index1 = new Index\ArrayIndex(); + $index2 = new Index\FixedArrayIndex(); + $index2->writeMode(); foreach ($data as $id => $values) { - $index->addRecord($id, $values); + $index1->addRecord($id, $values); + $index2->addRecord($id, $values); } - $search = new Search($index); + $index2->commitChanges(); + + $search = new Search($index1); $results = $search->find([new ValueFilter('tag', 1)]); + $sorter = new ByField($index1); + $sorted = $sorter->sort($results, 'size', ByField::SORT_DESC); + $this->assertEquals([3, 1, 4, 2], $sorted); - $sorter = new ByField($index); + $search = new Search($index2); + $results = $search->find([new ValueFilter('tag', 1)]); + $sorter = new ByField($index2); $sorted = $sorter->sort($results, 'size', ByField::SORT_DESC); $this->assertEquals([3, 1, 4, 2], $sorted); } @@ -39,14 +49,24 @@ public function testSortAsc() 4 => ['size' => 8, 'tag' => 1], 5 => ['size' => 8, 'tag' => 2] ]; - $index = new Index(); + $index1 = new Index\ArrayIndex(); + $index2 = new Index\FixedArrayIndex(); + $index2->writeMode(); foreach ($data as $id => $values) { - $index->addRecord($id, $values); + $index1->addRecord($id, $values); + $index2->addRecord($id, $values); } - $search = new Search($index); + $index2->commitChanges(); + + $search = new Search($index1); $results = $search->find([new ValueFilter('tag', 1)]); + $sorter = new ByField($index1); + $sorted = $sorter->sort($results, 'size', ByField::SORT_ASC); + $this->assertEquals([2, 4, 1, 3], $sorted); - $sorter = new ByField($index); + $search = new Search($index2); + $results = $search->find([new ValueFilter('tag', 1)]); + $sorter = new ByField($index2); $sorted = $sorter->sort($results, 'size', ByField::SORT_ASC); $this->assertEquals([2, 4, 1, 3], $sorted); } From 9cfd1d0fe25e953ad789249ad77a03b092521023 Mon Sep 17 00:00:00 2001 From: k-samuel Date: Fri, 31 Dec 2021 06:10:35 +0300 Subject: [PATCH 2/6] Readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a65d7bf..3be0f37 100644 --- a/README.md +++ b/README.md @@ -284,4 +284,4 @@ file_put_contents('./first-index.json', json_encode($indexData)); # Q&A * [Is it possible somehow to implement a full-text filter?](https://github.com/k-samuel/faceted-search/issues/3) * [Would that be possible to use a DB as an index instead of a json file?](https://github.com/k-samuel/faceted-search/issues/5) -* [Article about project history and base concepts of (in Russian)](https://habr.com/ru/post/595765/) \ No newline at end of file +* [Article about project history and base concepts (in Russian)](https://habr.com/ru/post/595765/) \ No newline at end of file From 224c8684fdea42c4022c386ed86eb97cdd0893d2 Mon Sep 17 00:00:00 2001 From: k-samuel Date: Fri, 31 Dec 2021 06:25:50 +0300 Subject: [PATCH 3/6] Readme update --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 3be0f37..089b872 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,11 @@ Also, you can create your own indexers with range detection method FixedArrayIndex is much slower but requires significant less memory. Working with an FixedArrayIndex is slightly different from ArrayIndex +The stored index data is compatible, you can transfer it from ArrayIndex to FixedArrayIndex + ```php commitChanges(); $indexData = $searchIndex->getData(); // We will use file for example file_put_contents('./first-index.json', json_encode($indexData)); + +// Index data is fully compatible. You can create both indexes from the same data +$arrayIndex = new ArrayIndex(); +$arrayIndex->setData($indexData); + + ``` From 7c59f132f97954bf1e29c18c710f75d9a0334c7d Mon Sep 17 00:00:00 2001 From: k-samuel Date: Sun, 2 Jan 2022 16:32:36 +0300 Subject: [PATCH 4/6] filters sorting moved to level up --- src/Search.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Search.php b/src/Search.php index c68f960..d29063e 100644 --- a/src/Search.php +++ b/src/Search.php @@ -66,6 +66,13 @@ public function find(array $filters, ?array $inputRecords = null): array if (!empty($inputRecords)) { $input = $this->mapInputArray($inputRecords); } + + // Aggregates optimisation for value filters. + // The fewer elements after the first filtering, the fewer data copies and memory allocations in iterations + if (empty($inputRecords) && count($filters) > 1) { + $filters = $this->sortFiltersByCount($filters); + } + return array_keys($this->findRecordsMap($filters, $input)); } @@ -102,12 +109,6 @@ private function findRecordsMap(array $filters, array $inputRecords): array return $total; } - // Aggregates optimisation for value filters. - // The fewer elements after the first filtering, the fewer data copies and memory allocations in iterations - if (empty($inputRecords) && count($filters) > 1) { - $filters = $this->sortFiltersByCount($filters); - } - /** * @var FilterInterface $filter */ @@ -139,6 +140,12 @@ private function findFilters(array $filters = [], array $inputRecords = [], bool $input = $this->mapInputArray($inputRecords); } + // Aggregates optimisation for value filters. + // The fewer elements after the first filtering, the fewer data copies and memory allocations in iterations + if (empty($inputRecords) && count($filters) > 1) { + $filters = $this->sortFiltersByCount($filters); + } + $result = []; $facetsData = $this->index->getData(); $indexedFilters = []; From b49026f9a67fee9f26769cb684b3d956158ba146 Mon Sep 17 00:00:00 2001 From: k-samuel Date: Sun, 2 Jan 2022 17:42:15 +0300 Subject: [PATCH 5/6] Added sorting of filter fields before processing (performance) --- README.md | 24 ++++++++++++------------ changelog.md | 17 +++++++++-------- src/Search.php | 13 +++++++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 089b872..d42a530 100644 --- a/README.md +++ b/README.md @@ -61,20 +61,20 @@ PHPBench v2.1.0 ArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | |----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| | 10,000 | ~6Mb | ~0.0003 s. | ~0.002 s. | ~0.0001 s. | 907 | -| 50,000 | ~40Mb | ~0.001 s. | ~0.013 s. | ~0.0006 s. | 4550 | -| 100,000 | ~80Mb | ~0.003 s. | ~0.029 s. | ~0.001 s. | 8817 | -| 300,000 | ~189Mb | ~0.011 s. | ~0.108 s. | ~0.005 s. | 26891 | -| 1,000,000 | ~657Mb | ~0.052 s. | ~0.419 s. | ~0.018 s. | 90520 | +| 50,000 | ~40Mb | ~0.001 s. | ~0.013 s. | ~0.0005 s. | 4550 | +| 100,000 | ~80Mb | ~0.003 s. | ~0.030 s. | ~0.001 s. | 8817 | +| 300,000 | ~189Mb | ~0.011 s. | ~0.106 s. | ~0.005 s. | 26891 | +| 1,000,000 | ~657Mb | ~0.050 s. | ~0.420 s. | ~0.018 s. | 90520 | PHPBench v2.1.0 FixedArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | |----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| | 10,000 | ~2Mb | ~0.0007 s. | ~0.006 s. | ~0.0002 s. | 907 | -| 50,000 | ~12Mb | ~0.003 s. | ~0.029 s. | ~0.001 s. | 4550 | -| 100,000 | ~23Mb | ~0.007 s. | ~0.058 s. | ~0.002 s. | 8817 | -| 300,000 | ~70Mb | ~0.021 s. | ~0.191 s. | ~0.008 s. | 26891 | -| 1,000,000 | ~233Mb | ~0.081 s. | ~0.702 s. | ~0.033 s. | 90520 | +| 50,000 | ~12Mb | ~0.003 s. | ~0.028 s. | ~0.001 s. | 4550 | +| 100,000 | ~23Mb | ~0.006 s. | ~0.059 s. | ~0.002 s. | 8817 | +| 300,000 | ~70Mb | ~0.021 s. | ~0.190 s. | ~0.008 s. | 26891 | +| 1,000,000 | ~233Mb | ~0.083 s. | ~0.700 s. | ~0.034 s. | 90520 | * Items count - Products in index * Memory - RAM used for index @@ -86,15 +86,15 @@ PHPBench v2.1.0 FixedArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) Experimental Golang port bench https://github.com/k-samuel/go-faceted-search -Bench v0.3.1 golang 1.17.3 with parallel aggregates +Bench v0.3.2 golang 1.17.3 with parallel aggregates | Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | |----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| | 10,000 | ~5Mb | ~0.0004 s. | ~0.001 s. | ~0.0002 s. | 907 | | 50,000 | ~15Mb | ~0.002 s. | ~0.010 s. | ~0.001 s. | 4550 | -| 100,000 | ~21Mb | ~0.006 s. | ~0.028 s. | ~0.002 s. | 8817 | -| 300,000 | ~47Mb | ~0.020 s. | ~0.091 s. | ~0.010 s. | 26891 | -| 1,000,000 | ~150Mb | ~0.089 s. | ~0.412 s. | ~0.034 s. | 90520 | +| 100,000 | ~21Mb | ~0.007 s. | ~0.030 s. | ~0.003 s. | 8817 | +| 300,000 | ~47Mb | ~0.021 s. | ~0.081 s. | ~0.007 s. | 26891 | +| 1,000,000 | ~150Mb | ~0.090 s. | ~0.372 s. | ~0.036 s. | 90520 | ## Examples diff --git a/changelog.md b/changelog.md index ead7117..d883aef 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ FixedArrayIndex is much slower but requires significant less memory * FixedArrayIndex added * KSamuel\FacetedSearch\Index is deprecated use KSamuel\FacetedSearch\Index\ArrayIndex instead * Unit and performance tests for FixedArrayIndex +* Added sorting of filter fields before processing (performance) * Documentation updated PHPBench v2.1.0 ArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) @@ -15,20 +16,20 @@ PHPBench v2.1.0 ArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | |----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| | 10,000 | ~6Mb | ~0.0003 s. | ~0.002 s. | ~0.0001 s. | 907 | -| 50,000 | ~40Mb | ~0.001 s. | ~0.013 s. | ~0.0006 s. | 4550 | -| 100,000 | ~80Mb | ~0.003 s. | ~0.029 s. | ~0.001 s. | 8817 | -| 300,000 | ~189Mb | ~0.011 s. | ~0.108 s. | ~0.005 s. | 26891 | -| 1,000,000 | ~657Mb | ~0.052 s. | ~0.419 s. | ~0.018 s. | 90520 | +| 50,000 | ~40Mb | ~0.001 s. | ~0.013 s. | ~0.0005 s. | 4550 | +| 100,000 | ~80Mb | ~0.003 s. | ~0.030 s. | ~0.001 s. | 8817 | +| 300,000 | ~189Mb | ~0.011 s. | ~0.106 s. | ~0.005 s. | 26891 | +| 1,000,000 | ~657Mb | ~0.050 s. | ~0.420 s. | ~0.018 s. | 90520 | PHPBench v2.1.0 FixedArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | |----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| | 10,000 | ~2Mb | ~0.0007 s. | ~0.006 s. | ~0.0002 s. | 907 | -| 50,000 | ~12Mb | ~0.003 s. | ~0.029 s. | ~0.001 s. | 4550 | -| 100,000 | ~23Mb | ~0.007 s. | ~0.058 s. | ~0.002 s. | 8817 | -| 300,000 | ~70Mb | ~0.021 s. | ~0.191 s. | ~0.008 s. | 26891 | -| 1,000,000 | ~233Mb | ~0.081 s. | ~0.702 s. | ~0.033 s. | 90520 | +| 50,000 | ~12Mb | ~0.003 s. | ~0.028 s. | ~0.001 s. | 4550 | +| 100,000 | ~23Mb | ~0.006 s. | ~0.059 s. | ~0.002 s. | 8817 | +| 300,000 | ~70Mb | ~0.021 s. | ~0.190 s. | ~0.008 s. | 26891 | +| 1,000,000 | ~233Mb | ~0.083 s. | ~0.700 s. | ~0.034 s. | 90520 | ### v2.0.3 (30.12.2021) Performance update diff --git a/src/Search.php b/src/Search.php index d29063e..52c3e4a 100644 --- a/src/Search.php +++ b/src/Search.php @@ -280,8 +280,14 @@ private function sortFiltersByCount(array $filters): array */ $filterValues = $filter->getValue(); + $filterValuesCount = []; + $valuesInFilter = count($filterValues); foreach ($filterValues as $value) { $cnt = $this->index->getRecordsCount($fieldName, $value); + if($valuesInFilter > 1){ + $filterValuesCount[$value] = $cnt; + } + if (!isset($counts[$index])) { $counts[$index] = $cnt; continue; @@ -291,6 +297,13 @@ private function sortFiltersByCount(array $filters): array $counts[$index] = $cnt; } } + + if($valuesInFilter > 1){ + // sort filter values by records count + asort($filterValuesCount); + // update filers with new values order + $filter->setValue(array_keys($filterValuesCount)); + } } asort($counts); $result = []; From f81bb8fee7e97ecee0d9e3b4d178ac11f26f8b29 Mon Sep 17 00:00:00 2001 From: k-samuel Date: Thu, 6 Jan 2022 16:37:52 +0300 Subject: [PATCH 6/6] Changelog update --- README.md | 12 ++++++------ changelog.md | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d42a530..4ff6565 100644 --- a/README.md +++ b/README.md @@ -63,18 +63,18 @@ PHPBench v2.1.0 ArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | 10,000 | ~6Mb | ~0.0003 s. | ~0.002 s. | ~0.0001 s. | 907 | | 50,000 | ~40Mb | ~0.001 s. | ~0.013 s. | ~0.0005 s. | 4550 | | 100,000 | ~80Mb | ~0.003 s. | ~0.030 s. | ~0.001 s. | 8817 | -| 300,000 | ~189Mb | ~0.011 s. | ~0.106 s. | ~0.005 s. | 26891 | -| 1,000,000 | ~657Mb | ~0.050 s. | ~0.420 s. | ~0.018 s. | 90520 | +| 300,000 | ~189Mb | ~0.011 s. | ~0.101 s. | ~0.005 s. | 26891 | +| 1,000,000 | ~657Mb | ~0.049 s. | ~0.396 s. | ~0.017 s. | 90520 | PHPBench v2.1.0 FixedArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | |----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| | 10,000 | ~2Mb | ~0.0007 s. | ~0.006 s. | ~0.0002 s. | 907 | -| 50,000 | ~12Mb | ~0.003 s. | ~0.028 s. | ~0.001 s. | 4550 | -| 100,000 | ~23Mb | ~0.006 s. | ~0.059 s. | ~0.002 s. | 8817 | -| 300,000 | ~70Mb | ~0.021 s. | ~0.190 s. | ~0.008 s. | 26891 | -| 1,000,000 | ~233Mb | ~0.083 s. | ~0.700 s. | ~0.034 s. | 90520 | +| 50,000 | ~12Mb | ~0.003 s. | ~0.027 s. | ~0.001 s. | 4550 | +| 100,000 | ~23Mb | ~0.006 s. | ~0.057 s. | ~0.002 s. | 8817 | +| 300,000 | ~70Mb | ~0.021 s. | ~0.188 s. | ~0.007 s. | 26891 | +| 1,000,000 | ~233Mb | ~0.080 s. | ~0.674 s. | ~0.032 s. | 90520 | * Items count - Products in index * Memory - RAM used for index diff --git a/changelog.md b/changelog.md index d883aef..f029f76 100644 --- a/changelog.md +++ b/changelog.md @@ -1,9 +1,9 @@ # Changelog -### v2.1.0 +### v2.1.0 (06.01.2022) -FixedArrayIndex +### Performance update and FixedArrayIndex -FixedArrayIndex is much slower but requires significant less memory +FixedArrayIndex is much slower than ArrayIndex but requires significant less memory. * FixedArrayIndex added * KSamuel\FacetedSearch\Index is deprecated use KSamuel\FacetedSearch\Index\ArrayIndex instead @@ -18,18 +18,18 @@ PHPBench v2.1.0 ArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | 10,000 | ~6Mb | ~0.0003 s. | ~0.002 s. | ~0.0001 s. | 907 | | 50,000 | ~40Mb | ~0.001 s. | ~0.013 s. | ~0.0005 s. | 4550 | | 100,000 | ~80Mb | ~0.003 s. | ~0.030 s. | ~0.001 s. | 8817 | -| 300,000 | ~189Mb | ~0.011 s. | ~0.106 s. | ~0.005 s. | 26891 | -| 1,000,000 | ~657Mb | ~0.050 s. | ~0.420 s. | ~0.018 s. | 90520 | +| 300,000 | ~189Mb | ~0.011 s. | ~0.101 s. | ~0.005 s. | 26891 | +| 1,000,000 | ~657Mb | ~0.049 s. | ~0.396 s. | ~0.017 s. | 90520 | PHPBench v2.1.0 FixedArrayIndex PHP 8.1.0 + JIT + opcache (no xdebug extension) | Items count | Memory | Find | Get Filters (aggregates) | Sort by field| Results Found | |----------------:|---------:|-----------------:|-------------------------:|-------------:|-----------------:| | 10,000 | ~2Mb | ~0.0007 s. | ~0.006 s. | ~0.0002 s. | 907 | -| 50,000 | ~12Mb | ~0.003 s. | ~0.028 s. | ~0.001 s. | 4550 | -| 100,000 | ~23Mb | ~0.006 s. | ~0.059 s. | ~0.002 s. | 8817 | -| 300,000 | ~70Mb | ~0.021 s. | ~0.190 s. | ~0.008 s. | 26891 | -| 1,000,000 | ~233Mb | ~0.083 s. | ~0.700 s. | ~0.034 s. | 90520 | +| 50,000 | ~12Mb | ~0.003 s. | ~0.027 s. | ~0.001 s. | 4550 | +| 100,000 | ~23Mb | ~0.006 s. | ~0.057 s. | ~0.002 s. | 8817 | +| 300,000 | ~70Mb | ~0.021 s. | ~0.188 s. | ~0.007 s. | 26891 | +| 1,000,000 | ~233Mb | ~0.080 s. | ~0.674 s. | ~0.032 s. | 90520 | ### v2.0.3 (30.12.2021) Performance update