Browse Source

Tweak algolia indexing, implement json search API, add faceting, etc

Jordi Boggiano 7 years ago
parent
commit
9ec4323d37

+ 56 - 5
composer.lock

@@ -4,8 +4,59 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "6e3fe09787ae05bb8046888d99c5bf9c",
+    "content-hash": "3997486ee7be835942f6cf3161f38fbb",
     "packages": [
+        {
+            "name": "algolia/algoliasearch-client-php",
+            "version": "1.20.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/algolia/algoliasearch-client-php.git",
+                "reference": "3daee8d55c1d84eff227fd99054e78ddc196f309"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/algolia/algoliasearch-client-php/zipball/3daee8d55c1d84eff227fd99054e78ddc196f309",
+                "reference": "3daee8d55c1d84eff227fd99054e78ddc196f309",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "php": ">=5.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8 || ^5.0",
+                "satooshi/php-coveralls": "0.6.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "AlgoliaSearch": "src/",
+                    "AlgoliaSearch\\Tests": "tests/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Algolia Team",
+                    "email": "contact@algolia.com"
+                },
+                {
+                    "name": "Ryan T. Catlin",
+                    "email": "ryan.catlin@gmail.com"
+                },
+                {
+                    "name": "Jonathan H. Wage",
+                    "email": "jonwage@gmail.com"
+                }
+            ],
+            "description": "Algolia Search API Client for PHP",
+            "homepage": "https://github.com/algolia/algoliasearch-client-php",
+            "time": "2017-08-30T08:28:40+00:00"
+        },
         {
             "name": "cebe/markdown",
             "version": "1.1.1",
@@ -130,12 +181,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/composer.git",
-                "reference": "a019fe7e6fe2a408f36417a455fc61190c451a01"
+                "reference": "e12e0335a4d467878093fc76e3dea592ff3e1ce3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/composer/zipball/a019fe7e6fe2a408f36417a455fc61190c451a01",
-                "reference": "a019fe7e6fe2a408f36417a455fc61190c451a01",
+                "url": "https://api.github.com/repos/composer/composer/zipball/e12e0335a4d467878093fc76e3dea592ff3e1ce3",
+                "reference": "e12e0335a4d467878093fc76e3dea592ff3e1ce3",
                 "shasum": ""
             },
             "require": {
@@ -199,7 +250,7 @@
                 "dependency",
                 "package"
             ],
-            "time": "2017-08-18T12:14:31+00:00"
+            "time": "2017-09-12T07:24:44+00:00"
         },
         {
             "name": "composer/semver",

+ 0 - 3
src/Packagist/WebBundle/Command/ConfigureAlgoliaCommand.php

@@ -17,9 +17,6 @@ use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Yaml\Yaml;
 
-/**
- * @author Igor Wiedler <igor@wiedler.ch>
- */
 class ConfigureAlgoliaCommand extends ContainerAwareCommand
 {
     /**

+ 57 - 51
src/Packagist/WebBundle/Command/IndexAlgoliaPackagesCommand.php

@@ -24,9 +24,6 @@ use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Filesystem\LockHandler;
 use Doctrine\DBAL\Connection;
 
-/**
- * @author Igor Wiedler <igor@wiedler.ch>
- */
 class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
 {
     /**
@@ -54,8 +51,7 @@ class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
         $force = $input->getOption('force');
         $indexAll = $input->getOption('all');
         $package = $input->getArgument('package');
-        $index_name = $this->getContainer()->getParameter('algolia.index_name');
-
+        $indexName = $this->getContainer()->getParameter('algolia.index_name');
 
         $deployLock = $this->getContainer()->getParameter('kernel.cache_dir').'/deploy.globallock';
         if (file_exists($deployLock)) {
@@ -67,7 +63,7 @@ class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
 
         $doctrine = $this->getContainer()->get('doctrine');
         $algolia = $this->getContainer()->get('packagist.algolia.client');
-        $index = $algolia->initIndex($index_name);
+        $index = $algolia->initIndex($indexName);
 
         $redis = $this->getContainer()->get('snc_redis.default');
         $downloadManager = $this->getContainer()->get('packagist.download_manager');
@@ -125,9 +121,9 @@ class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
                 }
 
                 try {
-                    $tags_formatted = $this->getTags($doctrine, $package);
+                    $tags = $this->getTags($doctrine, $package);
 
-                    $records[] = $this->packageToSearchableArray($package, $tags_formatted, $redis, $downloadManager, $favoriteManager);
+                    $records[] = $this->packageToSearchableArray($package, $tags, $redis, $downloadManager, $favoriteManager);
 
                     $idsToUpdate[] = $package->getId();
                 } catch (\Exception $e) {
@@ -136,34 +132,10 @@ class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
                     continue;
                 }
 
-//                $providers = $doctrine->getManager()->getConnection()->fetchAll(
-//                    'SELECT lp.packageName
-//                        FROM package p
-//                        JOIN package_version pv ON p.id = pv.package_id
-//                        JOIN link_provide lp ON lp.version_id = pv.id
-//                        WHERE p.id = :id
-//                        AND pv.development = true
-//                        GROUP BY lp.packageName',
-//                    ['id' => $package->getId()]
-//                );
-//                foreach ($providers as $provided) {
-//                    $provided = $provided['packageName'];
-//                    try {
-//                        $document = $update->createDocument();
-//                        $document->setField('id', $provided);
-//                        $document->setField('name', $provided);
-//                        $document->setField('package_name', '');
-//                        $document->setField('description', '');
-//                        $document->setField('type', 'virtual-package');
-//                        $document->setField('trendiness', 100);
-//                        $document->setField('repository', '');
-//                        $document->setField('abandoned', 0);
-//                        $document->setField('replacementPackage', '');
-//                        $update->addDocument($document);
-//                    } catch (\Exception $e) {
-//                        $output->writeln('<error>'.get_class($e).': '.$e->getMessage().', skipping package '.$package->getName().':provide:'.$provided.'</error>');
-//                    }
-//                }
+                $providers = $this->getProviders($doctrine, $package);
+                foreach ($providers as $provided) {
+                    $records[] = $this->createSearchableProvider($provided['packageName']);
+                }
             }
 
             try {
@@ -194,10 +166,11 @@ class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
         FavoriteManager $favoriteManager
     ) {
         $faversCount = $favoriteManager->getFaverCount($package);
-        $downloads = $downloadManager->getTotalDownloads($package);
-        $download_log = $package['downloads']['monthly'] ? log($package['downloads']['monthly'], 10) : 0;
-        $start_log = $package->getGitHubStars() ? log($package->getGitHubStars(), 10) : 0;
-        $popularity = round($download_log + $start_log);
+        $downloads = $downloadManager->getDownloads($package);
+        $downloadsLog = $downloads['monthly'] > 0 ? log($downloads['monthly'], 10) : 0;
+        $starsLog = $package->getGitHubStars() > 0 ? log($package->getGitHubStars(), 10) : 0;
+        $popularity = round($downloadsLog + $starsLog);
+        $trendiness = $redis->zscore('downloads:trending', $package->getId());
 
         $record = [
             'id' => $package->getId(),
@@ -205,19 +178,17 @@ class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
             'name' => $package->getName(),
             'package_organisation' => $package->getVendor(),
             'package_name' => $package->getPackageName(),
-            'description' => preg_replace('{[\x00-\x1f]+}u', '', $package->getDescription()),
+            'description' => preg_replace('{[\x00-\x1f]+}u', '', strip_tags($package->getDescription())),
             'type' => $package->getType(),
             'repository' => $package->getRepository(),
             'language' => $package->getLanguage(),
-            'trendiness' => $redis->zscore('downloads:trending', $package->getId()),
+            # log10 of downloads over the last 7days
+            'trendiness' => $trendiness > 0 ? log($trendiness, 10) : 0,
+            # log10 of downloads + gh stars
             'popularity' => $popularity,
             'meta' => [
-                'downloads' => $downloads,
-                'download_formatted' => [
-                    'total' => number_format($downloads['total'], 0, ',', ' '),
-                    'monthly' => number_format($downloads['monthly'], 0, ',', ' '),
-                    'daily' => number_format($downloads['daily'], 0, ',', ' '),
-                ],
+                'downloads' => $downloads['total'],
+                'downloads_formatted' => number_format($downloads['total'], 0, ',', ' '),
                 'favers' => $faversCount,
                 'favers_formatted' => number_format($faversCount, 0, ',', ' '),
             ],
@@ -231,13 +202,48 @@ class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
             $record['replacementPackage'] = '';
         }
 
-
         $record['tags'] = $tags;
 
         return $record;
     }
 
-    private function getTags($doctrine, $package)
+    private function createSearchableProvider(string $provided)
+    {
+        $record = [
+            'id' => $provided,
+            'objectID' => $provided,
+            'name' => $provided,
+            'package_organisation' => preg_replace('{/.*$}', '', $provided),
+            'package_name' => preg_replace('{^[^/]*/}', '', $provided),
+            'description' => '',
+            'type' => 'virtual-package',
+            'repository' => '',
+            'language' => '',
+            'trendiness' => 100,
+            'popularity' => 4,
+            'abandoned' => 0,
+            'replacementPackage' => '',
+            'tags' => [],
+        ];
+
+        return $record;
+    }
+
+    private function getProviders($doctrine, Package $package)
+    {
+        return $doctrine->getManager()->getConnection()->fetchAll(
+            'SELECT lp.packageName
+                FROM package p
+                JOIN package_version pv ON p.id = pv.package_id
+                JOIN link_provide lp ON lp.version_id = pv.id
+                WHERE p.id = :id
+                AND pv.development = true
+                GROUP BY lp.packageName',
+            ['id' => $package->getId()]
+        );
+    }
+
+    private function getTags($doctrine, Package $package)
     {
         $tags = $doctrine->getManager()->getConnection()->fetchAll(
             'SELECT t.name FROM package p
@@ -258,7 +264,7 @@ class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
         }, $tags);
     }
 
-    private function updateIndexedAt($idsToUpdate, $doctrine, $time)
+    private function updateIndexedAt(array $idsToUpdate, $doctrine, string $time)
     {
         $retries = 5;
         // retry loop in case of a lock timeout

+ 97 - 138
src/Packagist/WebBundle/Controller/WebController.php

@@ -56,8 +56,7 @@ class WebController extends Controller
 
         $orderBysViewModel = $this->getOrderBysViewModel($req, $normalizedOrderBys);
         return $this->render('PackagistWebBundle:Web:searchForm.html.twig', array(
-            'searchForm' => $form->createView(),
-            'orderBys' => $orderBysViewModel
+            'searchQuery' => $req->query->get('search_query')['query'] ?? '',
         ));
     }
 
@@ -78,163 +77,123 @@ class WebController extends Controller
         $typeFilter = str_replace('%type%', '', $req->query->get('type'));
         $tagsFilter = $req->query->get('tags');
 
-        if ($req->query->has('search_query') || $typeFilter || $tagsFilter) {
-            /** @var $solarium \Solarium_Client */
-            $solarium = $this->get('solarium.client');
-            $select = $solarium->createSelect();
+        if ($req->getRequestFormat() !== 'json') {
+            return $this->render('PackagistWebBundle:Web:search.html.twig', ['packages' => []]);
+        }
 
-            // configure dismax
-            $dismax = $select->getDisMax();
-            $dismax->setQueryFields(array('name^4', 'package_name^4', 'description', 'tags', 'text', 'text_ngram', 'name_split^2'));
-            $dismax->setPhraseFields(array('description'));
-            $dismax->setBoostFunctions(array('log(trendiness)^10'));
-            $dismax->setMinimumMatch(1);
-            $dismax->setQueryParser('edismax');
+        if (!$req->query->has('search_query') && !$typeFilter && !$tagsFilter) {
+            return JsonResponse::create(array(
+                'error' => 'Missing search query, example: ?q=example'
+            ), 400)->setCallback($req->query->get('callback'));
+        }
 
-            // filter by type
-            if ($typeFilter) {
-                $filterQueryTerm = sprintf('type:"%s"', $select->getHelper()->escapeTerm($typeFilter));
-                $filterQuery = $select->createFilterQuery('type')->setQuery($filterQueryTerm);
-                $select->addFilterQuery($filterQuery);
-            }
+        $indexName = $this->container->getParameter('algolia.index_name');
+        $algolia = $this->get('packagist.algolia.client');
+        $index = $algolia->initIndex($indexName);
+        $query = '';
+        $queryParams = [];
 
-            // filter by tags
-            if ($tagsFilter) {
-                $tags = array();
-                foreach ((array) $tagsFilter as $tag) {
-                    $tags[] = $select->getHelper()->escapeTerm($tag);
-                }
-                $filterQueryTerm = sprintf('tags:("%s")', implode('" AND "', $tags));
-                $filterQuery = $select->createFilterQuery('tags')->setQuery($filterQueryTerm);
-                $select->addFilterQuery($filterQuery);
-            }
+        // filter by type
+        if ($typeFilter) {
+            $queryParams['filters'][] = 'type:'.$typeFilter;
+        }
 
-            if (!empty($filteredOrderBys)) {
-                $select->addSorts($normalizedOrderBys);
-            }
+        // filter by tags
+        if ($tagsFilter) {
 
-            $form->handleRequest($req);
-            if ($form->isValid()) {
-                $escapedQuery = $select->getHelper()->escapeTerm($form->getData()->getQuery());
-                $escapedQuery = preg_replace('/(^| )\\\\-(\S)/', '$1-$2', $escapedQuery);
-                $escapedQuery = preg_replace('/(^| )\\\\\+(\S)/', '$1+$2', $escapedQuery);
-                if ((substr_count($escapedQuery, '"') % 2) == 0) {
-                    $escapedQuery = str_replace('\\"', '"', $escapedQuery);
-                }
-                $select->setQuery($escapedQuery);
+            $tags = array();
+            foreach ((array) $tagsFilter as $tag) {
+                $tags[] = 'tags:'.$tag;
             }
+            $queryParams['filters'][] = '(' . implode(' OR ', $tags) . ')';
+        }
 
-            $paginator = new Pagerfanta(new SolariumAdapter($solarium, $select));
+        if (!empty($filteredOrderBys)) {
+            return JsonResponse::create(array(
+                'status' => 'error',
+                'message' => 'Search sorting is not available anymore',
+            ), 400)->setCallback($req->query->get('callback'));
+        }
 
-            $perPage = $req->query->getInt('per_page', 15);
-            if ($perPage <= 0 || $perPage > 100) {
-                if ($req->getRequestFormat() === 'json') {
-                    return JsonResponse::create(array(
-                        'status' => 'error',
-                        'message' => 'The optional packages per_page parameter must be an integer between 1 and 100 (default: 15)',
-                    ), 400)->setCallback($req->query->get('callback'));
-                }
+        $form->handleRequest($req);
+        if ($form->isValid()) {
+            $query = $form->getData()->getQuery();
+        }
 
-                $perPage = max(0, min(100, $perPage));
+        $perPage = $req->query->getInt('per_page', 15);
+        if ($perPage <= 0 || $perPage > 100) {
+           if ($req->getRequestFormat() === 'json') {
+                return JsonResponse::create(array(
+                    'status' => 'error',
+                    'message' => 'The optional packages per_page parameter must be an integer between 1 and 100 (default: 15)',
+                ), 400)->setCallback($req->query->get('callback'));
             }
-            $paginator->setMaxPerPage($perPage);
-
-            $paginator->setCurrentPage($req->query->get('page', 1), false, true);
 
-            $metadata = array();
-
-            foreach ($paginator as $package) {
-                if (is_numeric($package->id)) {
-                    $metadata['downloads'][$package->id] = $package->downloads;
-                    $metadata['favers'][$package->id] = $package->favers;
-                }
-            }
+            $perPage = max(0, min(100, $perPage));
+        }
 
-            if ($req->getRequestFormat() === 'json') {
-                try {
-                    $result = array(
-                        'results' => array(),
-                        'total' => $paginator->getNbResults(),
-                    );
-                } catch (\Solarium_Client_HttpException $e) {
-                    return JsonResponse::create(array(
-                        'status' => 'error',
-                        'message' => 'Could not connect to the search server',
-                    ), 500)->setCallback($req->query->get('callback'));
-                }
+        $queryParams['filters'] = implode(' AND ', $queryParams['filters']);
+        $queryParams['hitsPerPage'] = $perPage;
+        $queryParams['page'] = $req->query->get('page', 1) - 1;
 
-                foreach ($paginator as $package) {
-                    if (ctype_digit((string) $package->id)) {
-                        $url = $this->generateUrl('view_package', array('name' => $package->name), UrlGeneratorInterface::ABSOLUTE_URL);
-                    } else {
-                        $url = $this->generateUrl('view_providers', array('name' => $package->name), UrlGeneratorInterface::ABSOLUTE_URL);
-                    }
-
-                    $row = array(
-                        'name' => $package->name,
-                        'description' => $package->description ?: '',
-                        'url' => $url,
-                        'repository' => $package->repository,
-                    );
-                    if (is_numeric($package->id)) {
-                        $row['downloads'] = $metadata['downloads'][$package->id];
-                        $row['favers'] = $metadata['favers'][$package->id];
-                    } else {
-                        $row['virtual'] = true;
-                    }
-                    $result['results'][] = $row;
-                }
+        try {
+            $results = $index->search($query, $queryParams);
+        } catch (\Throwable $e) {
+            return JsonResponse::create(array(
+                'status' => 'error',
+                'message' => 'Could not connect to the search server',
+            ), 500)->setCallback($req->query->get('callback'));
+        }
 
-                if ($paginator->hasNextPage()) {
-                    $params = array(
-                        '_format' => 'json',
-                        'q' => $form->getData()->getQuery(),
-                        'page' => $paginator->getNextPage()
-                    );
-                    if ($tagsFilter) {
-                        $params['tags'] = (array) $tagsFilter;
-                    }
-                    if ($typeFilter) {
-                        $params['type'] = $typeFilter;
-                    }
-                    if ($perPage !== 15) {
-                        $params['per_page'] = $perPage;
-                    }
-                    $result['next'] = $this->generateUrl('search', $params, UrlGeneratorInterface::ABSOLUTE_URL);
-                }
+        $result = array(
+            'results' => array(),
+            'total' => $results['nbHits'],
+        );
 
-                return JsonResponse::create($result)->setCallback($req->query->get('callback'));
+        foreach ($results['hits'] as $package) {
+            if (ctype_digit((string) $package['id'])) {
+                $url = $this->generateUrl('view_package', array('name' => $package['name']), UrlGeneratorInterface::ABSOLUTE_URL);
+            } else {
+                $url = $this->generateUrl('view_providers', array('name' => $package['name']), UrlGeneratorInterface::ABSOLUTE_URL);
             }
 
-            if ($req->isXmlHttpRequest()) {
-                try {
-                    return $this->render('PackagistWebBundle:Web:search.html.twig', array(
-                        'packages' => $paginator,
-                        'meta' => $metadata,
-                        'noLayout' => true,
-                    ));
-                } catch (\Twig_Error_Runtime $e) {
-                    if (!$e->getPrevious() instanceof \Solarium_Client_HttpException) {
-                        throw $e;
-                    }
-                    return JsonResponse::create(array(
-                        'status' => 'error',
-                        'message' => 'Could not connect to the search server',
-                    ), 500)->setCallback($req->query->get('callback'));
-                }
+            $row = array(
+                'name' => $package['name'],
+                'description' => $package['description'] ?: '',
+                'url' => $url,
+                'repository' => $package['repository'],
+            );
+            if (ctype_digit((string) $package['id'])) {
+                $row['downloads'] = $package['meta']['downloads'];
+                $row['favers'] = $package['meta']['favers'];
+            } else {
+                $row['virtual'] = true;
             }
+            if (!empty($package['abandoned'])) {
+                $row['abandoned'] = $package['replacementPackage'] ?? true;
+            }
+            $result['results'][] = $row;
+        }
 
-            return $this->render('PackagistWebBundle:Web:search.html.twig', array(
-                'packages' => $paginator,
-                'meta' => $metadata,
-            ));
-        } elseif ($req->getRequestFormat() === 'json') {
-            return JsonResponse::create(array(
-                'error' => 'Missing search query, example: ?q=example'
-            ), 400)->setCallback($req->query->get('callback'));
+        if ($results['nbPages'] > $results['page'] + 1) {
+            $params = array(
+                '_format' => 'json',
+                'q' => $form->getData()->getQuery(),
+                'page' => $results['page'] + 2,
+            );
+            if ($tagsFilter) {
+                $params['tags'] = (array) $tagsFilter;
+            }
+            if ($typeFilter) {
+                $params['type'] = $typeFilter;
+            }
+            if ($perPage !== 15) {
+                $params['per_page'] = $perPage;
+            }
+            $result['next'] = $this->generateUrl('search', $params, UrlGeneratorInterface::ABSOLUTE_URL);
         }
 
-        return $this->render('PackagistWebBundle:Web:search.html.twig');
+        return JsonResponse::create($result)->setCallback($req->query->get('callback'));
     }
 
     /**

+ 11 - 0
src/Packagist/WebBundle/Resources/config/algolia_settings.yml

@@ -1,7 +1,9 @@
 searchableAttributes:
+    - "name"
     - "package_name"
     - "package_organisation"
     - "description"
+    - "tags"
 
 ranking:
     - "typo"
@@ -21,3 +23,12 @@ attributesToHighlight:
     - "package_name"
     - "package_organisation"
     - "description"
+
+ignorePlurals: true
+advancedSyntax: true
+separatorsToIndex:
+    - '/'
+
+attributesForFaceting:
+    - "type"
+    - "tags"

+ 34 - 0
src/Packagist/WebBundle/Resources/public/css/main.css

@@ -419,6 +419,36 @@ strong {
     text-decoration: none;
 }
 
+#search-container {
+    margin-top: -20px;
+}
+
+#search-container em {
+    font-style: normal;
+    background: rgba(255, 255, 0, 0.16);
+    box-shadow: 0px 0px 2px rgba(210, 210, 91, 0.67);
+    border-radius: 3px;
+    font-weight: inherit;
+}
+
+.search-facets {
+    border-radius: 2px;
+    padding: 0 15px;
+    margin-top: 35px;
+}
+.search-facets > div:not(:last-child) {
+    margin-bottom: 15px;
+}
+.search-facets .ais-menu--item__active {
+    font-weight: bold;
+}
+.search-facets .ais-header {
+    border-bottom: 1px solid #f28d1a;
+    padding-bottom: 5px;
+    margin-bottom: 10px;
+    font-weight: bold;
+}
+
 .wrapper-footer {
     background: #2d2d32;
     font-size: 14px;
@@ -1089,6 +1119,10 @@ input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:fo
 }
 
 @media (min-width: 992px) {
+    .search-facets .ais-menu--count {
+        float: right;
+    }
+
     .package-aside .details {
       padding-left: 15px;
     }

+ 43 - 12
src/Packagist/WebBundle/Resources/public/js/search.js

@@ -6,10 +6,11 @@ var search = instantsearch({
     searchFunction: function(helper) {
         var searchResults = $('#search-container');
         if (helper.state.query === '') {
-            searchResults.hide();
+            searchResults.addClass('hidden');
+        } else {
+            helper.search();
+            searchResults.removeClass('hidden');
         }
-        helper.search();
-        searchResults.show();
     }
 });
 
@@ -18,37 +19,45 @@ search.addWidget(
         container: '#search_query_query',
         magnifier: false,
         reset: false,
-        wrapInput: false
+        wrapInput: false,
+        autofocus: true,
+        //queryHook: function (query, search) {
+        //    search(query);
+        //}
     })
 );
 
 search.addWidget(
     instantsearch.widgets.hits({
         container: '.search-list',
+        transformData: function (hit) {
+            if (hit.type === 'virtual-package') {
+                hit.virtual = true;
+            }
+
+            return hit;
+        },
         templates: {
             empty: 'No packages found.',
             item: `
-<div data-url="{{ packageUrl }}" class="col-xs-12 package-item">
+<div data-url="/packages/{{ name }}" class="col-xs-12 package-item">
     <div class="row">
         <div class="col-sm-9 col-lg-10">
             <p class="pull-right language">{{ language }}</p>
             <h4 class="font-bold">
-                <a href="{{ packageUrl }}">{{ name }}</a>
+                <a href="/packages/{{ name }}" tabindex="2">{{{ _highlightResult.name.value }}}</a>
                 {{#virtual}}
                     <small>(Virtual Package)</small>
                 {{/virtual}}
-                {{#showAutoUpdateWarning}}
-                    <small>(Not Auto-Updated)</small>
-                {{/showAutoUpdateWarning}}
             </h4>
 
-            <p>{{ description }}</p>
+            <p>{{{ _highlightResult.description.value }}}</p>
 
             {{#abandoned}}
             <p class="abandoned">
                 <i class="glyphicon glyphicon-exclamation-sign"></i> Abandoned!
                 {{#replacementPackage}}
-                    See <a href="{{ replacementPackage.link }}">{{ replacementPackage.name }}</a>
+                    See <a href="/packages/{{ replacementPackage }}">{{ replacementPackage }}</a>
                 {{/replacementPackage}}
             </p>
             {{/abandoned}}
@@ -57,7 +66,7 @@ search.addWidget(
         <div class="col-sm-3 col-lg-2">
             {{#meta}}
                 <p class="metadata">
-                    <span class="metadata-block"><i class="glyphicon glyphicon-arrow-down"></i> {{ meta.download_formatted.total }}</span>
+                    <span class="metadata-block"><i class="glyphicon glyphicon-arrow-down"></i> {{ meta.downloads_formatted }}</span>
                     <span class="metadata-block"><i class="glyphicon glyphicon-star"></i> {{ meta.favers_formatted }}</span>
                 </p>
             {{/meta}}
@@ -82,4 +91,26 @@ search.addWidget(
     })
 );
 
+search.addWidget(
+  instantsearch.widgets.menu({
+    container: '.search-facets-type',
+    attributeName: 'type',
+    limit: 15,
+    templates: {
+      header: 'Package type'
+    }
+  })
+);
+
+search.addWidget(
+  instantsearch.widgets.menu({
+    container: '.search-facets-tags',
+    attributeName: 'tags',
+    limit: 20,
+    templates: {
+      header: 'Tags'
+    }
+  })
+);
+
 search.start();

+ 0 - 17
src/Packagist/WebBundle/Resources/views/Web/search.html.twig

@@ -1,21 +1,4 @@
 {% embed "PackagistWebBundle:Web:list.html.twig" %}
-    {% block search_results %}
-    {% endblock %}
-
     {% block content %}
-        <div class="search-list">
-            {{ block('list') }}
-        </div>
-
-        <div id="powered-by">
-            Search by
-            <a href="https://www.algolia.com/">
-                <img src="{{ asset('bundles/packagistweb/img/algolia-logo-light.svg') }}">
-            </a>
-        </div>
-
-        <div id="pagination-container">
-            <div class="pagination"></div>
-        </div>
     {% endblock %}
 {% endembed %}

+ 5 - 9
src/Packagist/WebBundle/Resources/views/Web/searchForm.html.twig

@@ -1,11 +1,7 @@
-{{ form_start(searchForm, { attr: { id: 'search-form', autocomplete: 'off' } }) }}
-    <div class="{% if orderBys is defined %}sortable{% endif %} row">
-        <div class="hidden">{{ form_errors(searchForm.query) }}</div>
-        <div class="{% if searchForm.vars.value.query is empty %}col-xs-12{% else %}col-xs-8{% endif %} js-search-field-wrapper col-md-9">
-            {{ form_widget(searchForm.query, {'attr': {'autocomplete': 'off', 'placeholder': 'Search packages...', 'tabindex': 1}}) }}
+ <form name="search_query" method="get" action="/search/" id="search-form" autocomplete="off">
+    <div class="sortable row">
+        <div class="col-xs-12 js-search-field-wrapper col-md-9">
+            <input type="search" id="search_query_query" name="search_query[query]" required="required" autocomplete="off" placeholder="Search packages..." tabindex="1" class=" form-control" value="{{ searchQuery }}" />
         </div>
-
-        {{ form_rest(searchForm) }}
-
     </div>
-{{ form_end(searchForm) }}
+</form>

+ 20 - 8
src/Packagist/WebBundle/Resources/views/layout.html.twig

@@ -138,13 +138,25 @@
         <section class="wrapper">
             <section class="container content" role="main">
                 {% block search_results %}
-                    <div id="search-container" class="row">
-                        <div class="search-list col-md-12"></div>
-                        <div id="powered-by">
-                            Search by <a href="https://www.algolia.com/"><img src="{{ asset('bundles/packagistweb/img/algolia-logo-light.svg') }}"></a>
+                    <div id="search-container" class="hidden">
+                        <div class="row">
+                            <div class="search-list col-md-9"></div>
+
+                            <div class="search-facets col-md-3">
+                                <div class="search-facets-type"></div>
+                                <div class="search-facets-tags"></div>
+                            </div>
                         </div>
-                        <div id="pagination-container">
-                            <div class="pagination"></div>
+
+                        <div class="row">
+                            <div class="col-md-9">
+                                <div id="powered-by">
+                                    Search by <a href="https://www.algolia.com/"><img src="{{ asset('bundles/packagistweb/img/algolia-logo-light.svg') }}"></a>
+                                </div>
+                                <div id="pagination-container">
+                                    <div class="pagination"></div>
+                                </div>
+                            </div>
                         </div>
                     </div>
                 {% endblock %}
@@ -195,10 +207,10 @@
 
         <script src="{{ asset('libs/jquery-2.1.4.min.js') }}"></script>
         <script src="{{ asset('libs/humane-3.2.2.min.js') }}"></script>
-        <script src="{{ asset('libs/algolia-instantsearch-2.0.2/instantsearch.min.js') }}"></script>
         <script src="{{ asset('bundles/packagistweb/js/layout.js') }}"></script>
-        <script src="{{ asset('bundles/packagistweb/js/search.js')}}"></script>
         <script src="{{ asset('libs/bootstrap-3.3.5/js/bootstrap.min.js')}}"></script>
+        <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@1/dist/instantsearch-preact.min.js"></script>
+        <script src="{{ asset('bundles/packagistweb/js/search.js') }}"></script>
 
         {%- if not app.debug and google_analytics.ga_key %}
         <script>

File diff suppressed because it is too large
+ 0 - 1
web/libs/algolia-instantsearch-2.0.2/instantsearch.min.js


Some files were not shown because too many files changed in this diff