Статьи

PINQ – проверка ваших наборов данных – граненый поиск

В первой части мы кратко рассказали об установке и базовом синтаксисе PINQ, порта PHP LINQ. В этой статье мы увидим, как использовать PINQ для имитации функции многогранного поиска с MySQL.

Мы не собираемся охватить весь аспект граненого поиска в этой серии. Заинтересованные стороны могут ссылаться на соответствующие статьи, опубликованные на Sitepoint и другие публикации в Интернете.

Типичный граненый поиск работает так на сайте:

  • Пользователь предоставляет ключевое слово или несколько ключевых слов для поиска. Например, «маршрутизатор» для поиска продуктов, которые содержат «маршрутизатор» в описании, ключевое слово, категорию, теги и т. Д.
  • Сайт вернет товары, соответствующие критериям.
  • Сайт предоставит несколько ссылок для точной настройки поиска. Например, он может подсказать, что для маршрутизатора существуют разные бренды, и могут быть разные ценовые диапазоны и разные функции.
  • Пользователь может дополнительно просмотреть результаты, щелкнув по указанным ссылкам, и в итоге получит более индивидуальный набор результатов.

Фасетный поиск настолько популярен и мощен, что его можно увидеть практически на каждом сайте электронной коммерции.

К сожалению, граненый поиск еще не является встроенной функцией MySQL. Что мы можем сделать, если мы используем MySQL, но также хотим предоставить нашим пользователям такую ​​функцию?

С PINQ мы увидим, что есть не менее мощный и простой подход к достижению этой цели, чем когда мы используем другие механизмы БД – по крайней мере, в некотором смысле.

Расширение части 1 демо

ПРИМЕЧАНИЕ. Весь код этой части и демонстрационной части 1 можно найти в репозитории .

В этой статье мы расширим демоверсию, которую мы показали в первой части, и добавим некоторые важные функции многогранного поиска.

Давайте начнем с index.php

 $app->get('demo2', function () use ($app)
{
    global $demo;
    $test2 = new pinqDemo\Demo($app);
    return $test2->test2($app, $demo->test1($app));
}
);

$app->get('demo2/facet/{key}/{value}', function ($key, $value) use ($app)
{
    global $demo;
    $test3 = new pinqDemo\Demo($app);
    return $test3->test3($app, $demo->test1($app), $key, $value);
}
);

Мы только что создали еще два маршрута в нашем демонстрационном приложении (используя Silex).

Первый путь заключается в том, чтобы привести нас на страницу, показывающую все записи, которые соответствуют нашему первому поведению поиска, то есть поиск по ключевому слову. Чтобы сделать демонстрацию простой, мы выбираем все книги из примера таблицы book_book Он также будет отображать набор результатов и граненые ссылки для дальнейшей навигации.

Второй маршрут переносит нас на другую страницу, где отображаются записи, соответствующие дополнительным критериям поиска фасетов в наборе результатов, созданном на предыдущем шаге. Он также будет отображать граненые поисковые ссылки.

В реальной реализации после щелчка по фасетной ссылке любая фасетная фильтрация на странице результатов будет скорректирована с учетом статистической информации набора данных результатов. Таким образом, пользователь может применять показы «надстроек», сначала добавляя «бренд», затем «диапазон цен» и т. Д.

Но в этой простой демонстрации мы пропустим этот подход, весь поиск и ссылки будут отражать только информацию из исходного набора данных. Это первое ограничение и первая область для улучшения нашего демо.

Как видно из приведенного выше кода, реальные функции находятся в другом файле с именем pinqDemo.php Давайте посмотрим соответствующий код, который обеспечивает функцию граненого поиска.

Фасетный класс

Сначала мы создаем класс для представления фасета. Как правило, фасет должен иметь несколько свойств:

  • Данные, на которых он работает ( $data
  • Ключ, по которому он группируется ( $key
  • Тип ключа ( $type Это может быть один из следующих:
    • указать полную строку для точного соответствия
    • указать частичное (обычно начало) строки для сопоставления с шаблоном
    • указать диапазон значений для группировки по диапазону значений
  • Если тип ключа является диапазоном, необходимо указать шаг значения для определения верхней / нижней границы диапазона; или если тип ключа является частичной строкой, нам нужно указать число, чтобы указать, сколько первых букв будет использовано для группировки ( $range

Группировка является наиболее важной частью аспекта. Вся агрегирующая информация, которую фасет может вернуть, зависит от критериев «группировки». Обычно наиболее часто используются «Полная строка», «Частичная строка» и «Диапазон значений».

 namespace classFacet
{
    use Pinq\ITraversable,
        Pinq\Traversable;

    class Facet
    {

        public $data; // Original data
        public $key; // the field to be grouped on
        public $type; // F: full string; S: start of a string; R: range;
        public $range; // Only valid if $type is not F

		...

        public function getFacet()
        {
            $filter = '';

            if ($this->type == 'F') // Full string 
            {
				...
            }
            elseif ($this->type == "S") //Start of string
            {
				...
            }
            elseif ($this->type == "R") // A value range
            {
                $filter = $this->data
                        ->groupBy(function($row)
                        {
                            return floor($row[$this->key] / $this->range) * $this->range;
                        })
                        ->select(function (ITraversable $data)
                {
                    return ['key' => $data->last()[$this->key], 'count' => $data->count()];
                });
            }

            return $filter;
        }
    }
}

В этом классе ключевая функция должна возвращать набор граненых результатов на основе данных и свойств ключа фасета. Мы заметили, что для разных типов ключей существуют разные способы группировки данных. Выше мы показали, как будет выглядеть код, если мы сгруппируем данные по диапазону значений на шаге, указанном $range

Создание граней и отображение исходных данных

 public function test2($app, $data)
        {
            $facet = $this->getFacet($data);
            return $app['twig']->render('demo2.html.twig', array('facet' => $facet, 'data' => $data));
        }

		private function getFacet($originalData)
        {
            $facet = array();

            $data = \Pinq\Traversable::from($originalData);

            // 3 samples on constructing different Facet objects and return the facet
            $filter1 = new \classFacet\Facet($data, 'author', 'F');
            $filter2 = new \classFacet\Facet($data, 'title', 'S', 6);
            $filter3 = new \classFacet\Facet($data, 'price', 'R', 10);

            $facet[$filter1->key] = $filter1->getFacet();
            $facet[$filter2->key] = $filter2->getFacet();
            $facet[$filter3->key] = $filter3->getFacet();
            return $facet;
        }

В функции getFacet()

  • Преобразуйте исходные данные в объект Pinq\Traversable
  • Мы создаем 3 аспекта. Фасет ‘author’ будет группироваться по author фасет ‘title’ в title фасет ‘price’ для price
  • Наконец, мы получаем фасеты и возвращаем их обратно в функцию test2

Отображение фасетов и отфильтрованных данных

В большинстве случаев фасеты будут отображаться в виде ссылки и приводят нас к отфильтрованному набору данных.

Мы уже создали маршрут ( 'demo2/facet/{key}/{value}'

Маршрут принимает два параметра, отражающих ключ, с которым мы сталкиваемся, и значение этого ключа. Функция test3

 public function test3($app, $originalData, $key, $value)
        {
            $data = \Pinq\Traversable::from($originalData);
            $facet = $this->getFacet($data);

            $filter = null;

            if ($key == 'author')
            {
                $filter = $data
                        ->where(function($row) use ($value)
                        {
                            return $row['author'] == $value;
                        })
                        ->orderByAscending(function($row) use ($key)
                {
                    return $row['price'];
                })
                ;
            }
            elseif ($key == 'price')
            {
				...
            }
            else //$key==title
            {
                ...
            }

            return $app['twig']->render('demo2.html.twig', array('facet' => $facet, 'data' => $filter));
        }

В основном, в зависимости от ключа, мы применяем фильтрацию (анонимная функция в предложении where Мы также можем указать порядок граненых данных.

Наконец, мы отображаем данные (вместе с фасетами) в шаблоне. Этот маршрут отображает тот же шаблон, который используется маршрутом 'demo2'

Далее, давайте посмотрим на шаблон и посмотрим, как отображаются фасетные ссылки. Я использую Bootstrap, поэтому используемые здесь компоненты CSS должны быть достаточно знакомы:

 <div class="col col-md-4">
                <h4>Search Bar</h4>
                <ul>
                    {% for k, v in facet %}
                        <li><h5><strong>{{k|capitalize}}</strong></h5></li>
                        <ul class="list-group">
                            {% for vv in v %}
                                <li class="list-group-item"><span class="badge">{{vv.count}}</span><a href="/demo2/facet/{{k}}/{{vv.key}}">{{vv.key}}</a></li>
                            {%endfor%}
                        </ul>
                    {%endfor%}
                </ul>
</div>

Мы должны помнить, что фасет, сгенерированный нашим приложением, является вложенным массивом. На первом уровне это массив всех аспектов, и в нашем случае у нас их всего 3 (для authortitleauthor

Для каждого аспекта это парный массив «ключ-значение», поэтому мы можем выполнять итерации традиционным способом.

Обратите внимание, как мы строим URI ссылок. В качестве параметров в маршруте мы использовали и ключ внешнего цикла ( kvv.key'demo2/facet/{key}/{value}' Счетчик ключа ( vv.countauthor

Шаблон будет отображен как показано ниже:


(Первая показывает начальную страницу входа, а вторая показывает многогранный результат с ценой от 0 до 10 долларов и заказывается index.php

Хорошо, пока нам удалось имитировать функцию граненого поиска в нашем веб-приложении!

Прежде чем мы закончим эту серию, мы окончательно посмотрим на эту демонстрацию и посмотрим, что можно сделать, чтобы улучшить ее, и каковы ограничения.

Улучшения должны быть сделаны

В целом, это довольно элементарная демонстрация. Мы просто пробежались по основному синтаксису и понятиям и подделали их в пример, которым можно управлять Как мы видели ранее, несколько областей могут быть улучшены, чтобы сделать его более гибким.

Нам нужно рассмотреть возможность предоставления дополнительных критериев поиска. Наша текущая реализация ограничивает поиск фасетов, который будет применяться только к оригиналам, а не к проверенным данным. Это самое важное улучшение, которое я могу придумать.

Ограничения

Реализованный здесь фасетный поиск имеет глубоко укоренившееся ограничение (и, вероятно, верно для других реализаций граненого поиска):

Мы получаем данные с сервера MySQL каждый раз.

Это приложение использует Silex в качестве основы. Для любого фреймворка с одним входом, такого как Silex, Symfony, Laravel, его app.phpindex.php$demo = new pinqDemo\Demo($app);

Глядя на код в нашем class Demo
{

private $books = '';

public function __construct($app)
{
$sql = 'select * from book_book order by id';
$this->books = $app['db']->fetchAll($sql);
}

 select

вызывается каждый раз, когда отображается страница в приложении, что означает, что каждый раз выполняются следующие строки:

 group by

Будет ли лучше, если мы будем избегать использования фреймворка? Что ж, помимо того, что разработка приложения без фреймворка не очень хорошая идея, мы все еще сталкиваемся с той же проблемой: данные (и состояние) не являются постоянными от одного HTTP-вызова к другому. Это фундаментальная характеристика HTTP. Этого следует избегать с использованием механизма кэширования.

Мы сохраняем некоторые операторы SQL, выполняемые на стороне сервера, когда мы создаем фасеты. Вместо того, чтобы передавать 1 запрос на выборку и 3 различные whereselectwhere

Вывод

В этой части нам удалось имитировать возможность поиска по фасету для нашего сайта коллекции книг. Как я уже сказал, это всего лишь демоверсия, которую можно запустить, и она имеет много возможностей для улучшения и некоторых ограничений по умолчанию. Дайте нам знать, если вы опираетесь на этот пример и можете показать нам несколько более сложных вариантов использования!

Автор PINQ сейчас работает над следующим основным выпуском версии (версия 3). Я надеюсь, что это может стать более сильным.

Не стесняйтесь оставлять свои комментарии и мысли ниже!