В первой части мы кратко рассказали об установке и базовом синтаксисе 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 (для author
title
author
Для каждого аспекта это парный массив «ключ-значение», поэтому мы можем выполнять итерации традиционным способом.
Обратите внимание, как мы строим URI ссылок. В качестве параметров в маршруте мы использовали и ключ внешнего цикла ( k
vv.key
'demo2/facet/{key}/{value}'
Счетчик ключа ( vv.count
author
Шаблон будет отображен как показано ниже:
(Первая показывает начальную страницу входа, а вторая показывает многогранный результат с ценой от 0 до 10 долларов и заказывается index.php
Хорошо, пока нам удалось имитировать функцию граненого поиска в нашем веб-приложении!
Прежде чем мы закончим эту серию, мы окончательно посмотрим на эту демонстрацию и посмотрим, что можно сделать, чтобы улучшить ее, и каковы ограничения.
Улучшения должны быть сделаны
В целом, это довольно элементарная демонстрация. Мы просто пробежались по основному синтаксису и понятиям и подделали их в пример, которым можно управлять Как мы видели ранее, несколько областей могут быть улучшены, чтобы сделать его более гибким.
Нам нужно рассмотреть возможность предоставления дополнительных критериев поиска. Наша текущая реализация ограничивает поиск фасетов, который будет применяться только к оригиналам, а не к проверенным данным. Это самое важное улучшение, которое я могу придумать.
Ограничения
Реализованный здесь фасетный поиск имеет глубоко укоренившееся ограничение (и, вероятно, верно для других реализаций граненого поиска):
Мы получаем данные с сервера MySQL каждый раз.
Это приложение использует Silex в качестве основы. Для любого фреймворка с одним входом, такого как Silex, Symfony, Laravel, его app.php
index.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 различные where
select
where
Вывод
В этой части нам удалось имитировать возможность поиска по фасету для нашего сайта коллекции книг. Как я уже сказал, это всего лишь демоверсия, которую можно запустить, и она имеет много возможностей для улучшения и некоторых ограничений по умолчанию. Дайте нам знать, если вы опираетесь на этот пример и можете показать нам несколько более сложных вариантов использования!
Автор PINQ сейчас работает над следующим основным выпуском версии (версия 3). Я надеюсь, что это может стать более сильным.
Не стесняйтесь оставлять свои комментарии и мысли ниже!