3
0

55 Commits

Author SHA1 Message Date
802413f754 Fixing deprecation 2023-01-09 16:07:13 +00:00
e56ea29018 Adding batching to index command for performance 2023-01-09 16:06:37 +00:00
3faec6370c Merge tag '0.0.18' into develop
Trying to load custom Doctrine config
2022-07-29 11:27:46 +01:00
361cd7baf8 Merge branch 'release/0.0.18' 2022-07-29 11:27:31 +01:00
6111bd07e5 Updating Doctrine settings 2022-07-29 11:27:21 +01:00
76ab724d1a Merge branch 'develop' of ssh://git.pcmdev.co.uk:2222/pcm-libraries/pcm-search-bundle into develop 2022-07-29 11:18:59 +01:00
d7a7d0b6e8 Merge tag '0.0.17' into develop
- Adding loader for doctrine.yaml to bring in MATCH AGAINST function
2022-07-29 11:18:49 +01:00
3f57c78d01 Merge branch 'release/0.0.17' 2022-07-29 11:18:35 +01:00
e88da82267 Added loader for doctrine.yaml to bring in MATCH AGAINST method 2022-07-29 11:18:20 +01:00
Brabli
709c641fc9 Migrate xml schema 2022-07-21 15:10:19 +01:00
Brabli
ff506e2aef Add tests command 2022-07-21 15:10:05 +01:00
Brabli
702fe24be4 Add space 2022-07-21 11:51:47 +01:00
319334834b Merge tag '0.0.16' into develop
Working on DI 0.0.16
2022-07-20 21:29:45 +01:00
9bad1cb2b9 Merge branch 'release/0.0.16' 2022-07-20 21:29:43 +01:00
df927a8197 Adding doctrine.yaml config 2022-07-20 21:29:33 +01:00
5c98989631 Merge tag '0.0.15' into develop
Working on DI 0.0.15
2022-07-20 21:26:13 +01:00
373087a5d5 Merge branch 'release/0.0.15' 2022-07-20 21:26:11 +01:00
205b29e07c Working on DependencyInjection 2022-07-20 21:26:00 +01:00
e1d5f094de Merge tag '0.0.14' into develop
Working on DI 0.0.14
2022-07-20 21:23:55 +01:00
8145b87242 Merge branch 'release/0.0.14' 2022-07-20 21:23:53 +01:00
d027933ad9 Working on DependencyInjection 2022-07-20 21:23:43 +01:00
cbad86a01c Merge tag '0.0.13' into develop
Working on DI 0.0.13
2022-07-20 21:21:37 +01:00
15bf7cb324 Merge branch 'release/0.0.13' 2022-07-20 21:21:35 +01:00
1b9787aa49 Working on DependencyInjection 2022-07-20 21:21:15 +01:00
9d4297659d Merge tag '0.0.12' into develop
Working on DI 0.0.12
2022-07-20 21:16:15 +01:00
92842c552b Merge branch 'release/0.0.12' 2022-07-20 21:16:13 +01:00
80e584e29f Working on DependencyInjection 2022-07-20 21:16:05 +01:00
c03d2ae9ad Merge tag '0.0.11' into develop
Working on DI 0.0.11
2022-07-20 21:15:24 +01:00
c27ae18aa0 Merge branch 'release/0.0.11' 2022-07-20 21:15:22 +01:00
89c88520d9 Working on DependencyInjection 2022-07-20 21:15:13 +01:00
c19a755ed1 Merge tag '0.0.10' into develop
Working on DI 0.0.10
2022-07-20 21:09:54 +01:00
12925cc7d7 Merge branch 'release/0.0.10' 2022-07-20 21:09:52 +01:00
393a704f1c Working on DependencyInjection 2022-07-20 21:09:43 +01:00
398e2903ea Merge tag '0.0.9' into develop
Working on DI 0.0.9
2022-07-20 21:06:17 +01:00
95db65f56d Merge branch 'release/0.0.9' 2022-07-20 21:06:16 +01:00
1ec9b05413 Working on DependencyInjection 2022-07-20 21:05:54 +01:00
6a4ce3bef3 Merge tag '0.0.8' into develop
Working on DI
2022-07-20 21:02:39 +01:00
bf89b962ec Merge branch 'release/0.0.8' 2022-07-20 21:02:33 +01:00
7de30e78ac Working on DependencyInjection 2022-07-20 21:02:17 +01:00
6d65df0b9c Merge tag '0.0.7' into develop
Working on DependencyInjection
2022-07-20 21:01:11 +01:00
5d56d98325 Merge branch 'release/0.0.7' 2022-07-20 21:01:06 +01:00
2f6597a957 Working on DependencyInjection 2022-07-20 21:00:54 +01:00
93bb85f69f Merge tag '0.0.6' into develop
Working on DependencyInjection
2022-07-20 20:54:28 +01:00
218b174d4f Merge branch 'release/0.0.6' 2022-07-20 20:54:19 +01:00
2258fa0c96 Working on DependencyInjection 2022-07-20 20:54:10 +01:00
34b0e7f146 Merge tag '0.0.5' into develop
Working on DependencyInjection
2022-07-20 20:47:30 +01:00
ab25540462 Merge branch 'release/0.0.5' 2022-07-20 20:47:22 +01:00
da950685c0 Working on DependencyInjection 2022-07-20 20:47:05 +01:00
954e6aa712 Merge tag '0.0.4' into develop
Working on DependencyInjection
2022-07-20 20:44:45 +01:00
3f410b8f55 Merge branch 'release/0.0.4' 2022-07-20 20:44:36 +01:00
78f356f030 Working on DependencyInjection 2022-07-20 20:44:25 +01:00
c35da20deb Merge tag '0.0.3' into develop
- Adding services.yaml
- Fleshing out the codebase further
2022-07-20 20:38:38 +01:00
066e3f0d4b Merge branch 'release/0.0.3' 2022-07-20 20:38:22 +01:00
cf03ff8f89 Fleshing out a bit further 2022-07-20 20:38:09 +01:00
c46af9c4f7 Merge tag '0.0.2' into develop
Fixing autoloader
2022-07-20 09:53:41 +01:00
13 changed files with 306 additions and 19 deletions

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
# This is the top-most .editorconfig file; do not search in parent directories.
root = true
# All files.
[*]
end_of_line = LF
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[Makefile]
indent_style = tab

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/vendor /vendor
composer.lock composer.lock
.phpunit.result.cache

View File

@@ -4,4 +4,4 @@ WORKDIR /code
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY ./ /code COPY ./ /code
RUN composer install RUN composer install

View File

@@ -1,8 +1,16 @@
PHP = docker compose run php PHP = docker compose run php
.PHONY: composer_install composer_update static_analysis tests
composer_install: composer_install:
@$(PHP) composer install @$(PHP) composer install
composer_update:
@$(PHP) composer update
static_analysis: static_analysis:
@$(PHP) vendor/bin/psalm @$(PHP) vendor/bin/psalm
tests:
@$(PHP) rm -rf var/cache
@$(PHP) vendor/bin/phpunit

View File

@@ -9,11 +9,21 @@
"doctrine/orm": "^2.12", "doctrine/orm": "^2.12",
"symfony/framework-bundle": "*" "symfony/framework-bundle": "*"
}, },
"require-dev": {
"vimeo/psalm": "^4.24",
"psalm/plugin-symfony": "^3.1",
"phpunit/phpunit": "^9.5"
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Pcm\\SearchBundle\\": "src/" "Pcm\\SearchBundle\\": "src/"
} }
}, },
"autoload-dev": {
"psr-4": {
"Pcm\\SearchBundle\\": "tests/"
}
},
"authors": [ "authors": [
{ {
"name": "Matt Feeny", "name": "Matt Feeny",
@@ -23,9 +33,5 @@
"name": "Bradley Goode", "name": "Bradley Goode",
"email": "bg@pcmsystems.co.uk" "email": "bg@pcmsystems.co.uk"
} }
], ]
"require-dev": {
"vimeo/psalm": "^4.24",
"psalm/plugin-symfony": "^3.1"
}
} }

25
config/services.yaml Normal file
View File

@@ -0,0 +1,25 @@
services:
_defaults:
autowire: true
autoconfigure: true
pcm_search.command.index:
class: Pcm\SearchBundle\Command\SearchIndexCommand
tags:
- { name: 'console.command', command: 'pcm:search:reindex' }
pcm_search.searchable_subscriber:
class: Pcm\SearchBundle\EventSubscriber\SearchableSubscriber
public: true
tags:
- { name: doctrine.event_subscriber }
pcm_search.search_service:
alias: Pcm\SearchBundle\Service\SearchService
public: true
Pcm\SearchBundle\Service\SearchService: ~
Pcm\SearchBundle\Repository\SearchIndexRepository:
autowire: true
tags: ['doctrine.repository_service']

19
phpunit.xml.dist Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="./vendor/autoload.php">
<coverage>
<include>
<directory>./src</directory>
</include>
</coverage>
<php>
<ini name="error_reporting" value="-1"/>
<ini name="intl.default_locale" value="en"/>
<ini name="intl.error_level" value="0"/>
<ini name="memory_limit" value="-1"/>
</php>
<testsuites>
<testsuite name="Test suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -2,10 +2,12 @@
namespace Pcm\SearchBundle\Command; namespace Pcm\SearchBundle\Command;
use Doctrine\ORM\EntityManagerInterface;
use Pcm\SearchBundle\Entity\SearchIndex; use Pcm\SearchBundle\Entity\SearchIndex;
use Pcm\SearchBundle\Service\SearchService; use Pcm\SearchBundle\Service\SearchService;
use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
@@ -23,7 +25,16 @@ class SearchIndexCommand extends Command
parent::__construct(); parent::__construct();
} }
protected function configure(): void {} protected function configure(): void
{
$this->addOption(
'batchsize',
'b',
InputOption::VALUE_REQUIRED,
'Number of rows to process before flushing',
2000
);
}
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{ {
@@ -42,29 +53,46 @@ class SearchIndexCommand extends Command
$progress_bar->setProgressCharacter("<fg=green>➤</>"); $progress_bar->setProgressCharacter("<fg=green>➤</>");
$progress_bar->start(); $progress_bar->start();
$batchSize = $input->getOption('batchsize');
$searchEntities = null;
foreach ($searchables as $searchable) { foreach ($searchables as $searchable) {
$entities = $this $entities = $this
->em ->em
->getRepository($searchable) ->getRepository($searchable)
->findAll(); ->findAll();
$searchEntities[] = [$searchable, count($entities)];
$this->em->clear();
$counter = 1;
foreach ($entities as $entity) { foreach ($entities as $entity) {
$search_result = $this->searchService->createSearchResult($entity); $search_result = $this->searchService->createSearchResult($entity);
$this->em->persist($search_result); $this->em->persist($search_result);
if (($counter % $batchSize) === 0) {
$this->em->flush();
$this->em->clear();
}
$counter++;
} }
$progress_bar->advance(); $progress_bar->advance();
$this->em->flush();
$this->em->clear();
} }
$this->em->flush();
$progress_bar->finish(); $progress_bar->finish();
$io->writeln(''); $table = new Table($output);
$io->writeln(''); $table
->setHeaders(['Class', 'Count'])
->setRows($searchEntities)
;
$table->render();
$io->writeln('');
$io->success('Index updated'); $io->success('Index updated');
return Command::SUCCESS; return Command::SUCCESS;

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Pcm\SearchBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class PcmSearchExtension extends Extension implements PrependExtensionInterface
{
public function prepend(ContainerBuilder $container)
{
$container->loadFromExtension(
'doctrine',
[
'orm' => [
'dql' => [
'string_functions' => [
'match' => 'DoctrineExtensions\Query\Mysql\MatchAgainst'
]
]
]
]
);
}
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../../config')
);
$loader->load('services.yaml');
}
}

View File

@@ -14,7 +14,7 @@ class SearchableSubscriber implements EventSubscriber
{ {
public function __construct(private SearchService $searchService) {} public function __construct(private SearchService $searchService) {}
public function getSubscribedEvents() public function getSubscribedEvents(): array
{ {
return [ return [
Events::postPersist, Events::postPersist,

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Pcm\SearchBundle\Repository;
use Pcm\SearchBundle\Entity\SearchIndex;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query;
use Doctrine\Persistence\ManagerRegistry;
class SearchIndexRepository extends ServiceEntityRepository
{
/**
* @param ManagerRegistry $registry
*/
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, SearchIndex::class);
}
/**
* Saves a SearchIndex entity
*
* @param SearchIndex $entity
* @param boolean $flush
* @return void
*/
public function add(SearchIndex $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* Removes a SearchIndex entity
*
* @param SearchIndex $entity
* @param boolean $flush
* @return void
*/
public function remove(SearchIndex $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* Returns a Query object ready to be paginated or used to present results.
*
* @param string $query
* @param integer $minScore
* @return Query
*/
public function findAllPagination(string $query, int $minScore = 0): Query
{
$qb = $this->createQueryBuilder('r')
->addSelect('MATCH(r.data) AGAINST(:searchText boolean) AS score')
->where(
sprintf(
'MATCH(r.data) AGAINST(:searchText boolean) > %f',
$minScore
)
)->orderBy('score', 'DESC')
->setParameter(
'searchText',
$this->convertSearchTerm($query)
);
return $qb->getQuery();
}
/**
* Takes a string $query and explodes into individual words. Each word
* is then prefixed with + and ends with *, making the full text search
* operate as wildcard on all words
*
* @param string $query
* @return string
*/
private function convertSearchTerm(string $query): string
{
$extractedWords = [];
$sanitisedString = preg_replace('/[^\w^\d]/', ' ', $query);
$words = mb_split('\s', preg_replace(['/([^\w+])/','/(\s+)/'], ' ', $sanitisedString));
foreach ($words as $word) {
if (strlen($word)< 1) {
//
continue;
}
$word = strtoupper($word);
$extractedWords[$word] = $word;
}
array_walk(
$extractedWords,
function(&$word) {
// require every word but allow matching just the start
$word = '+' . $word . '*';
}
);
return implode(' ', $extractedWords);
}
/**
* Clears the index table of all results
* @return void
*/
public function clearIndex(): void
{
$this
->createQueryBuilder('s')
->delete()
->getQuery()
->execute();
}
}

View File

@@ -14,7 +14,14 @@ class SearchService
{ {
public function __construct(private EntityManagerInterface $em) {} public function __construct(private EntityManagerInterface $em) {}
public function index($entity) /**
* Given an $entity that implements SearchableInterface, this method
* creates or updates a SearchIndex $entity
*
* @param SearchableInterface $entity
* @return void
*/
public function index(SearchableInterface $entity)
{ {
$searchIndex = $this->createSearchResult($entity); $searchIndex = $this->createSearchResult($entity);
@@ -22,7 +29,14 @@ class SearchService
$this->em->flush(); $this->em->flush();
} }
public function unIndex($entity) /**
* Given an $entity that implements SearchableInterface, this method removes
* the item from the search index
*
* @param SearchableInterface $entity
* @return void
*/
public function unIndex(SearchableInterface $entity)
{ {
$class = get_class($entity); $class = get_class($entity);
@@ -37,6 +51,14 @@ class SearchService
} }
} }
/**
* Given an entity that implements SearchableInterface, this method first checks
* if the relevant SearchIndex entity exists. If it doesn't, it's created. The
* title and index data are set based on the methods in the $entity
*
* @param SearchableInterface $entity
* @return SearchIndex
*/
public function createSearchResult(SearchableInterface $entity): SearchIndex public function createSearchResult(SearchableInterface $entity): SearchIndex
{ {
$values = []; $values = [];
@@ -76,7 +98,8 @@ class SearchService
} }
/** /**
* Finds all searchable Doctrine entities. * Finds all searchable Doctrine entities the implement SearchableInterface
* @return array
*/ */
public function getSearchableClasses(): array public function getSearchableClasses(): array
{ {

0
tests/.gitkeep Normal file
View File