Реализация поиска по сайту на Yii Framework 1.1 / Студия Виталия Комлева, разработка веб-сайтов Харьков

Реализация поиска по сайту на Yii Framework 1.1

Веб-дизайн и аналитика

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

Довольно часто возникает необходимость реализовать поиск по сайту, но при этом структура данных неравномерна. Например, на корпоративном сайте клиент хочет искать по каталогу товаров (среди самих категорий и отдельно по товарам), по статьям и по статическим страничкам. Решение должно быть удобно расширяемым, а алгоритм понятным даже через полгода.

Первый, очевидный вариант — составить большой SQL-запрос, и оперировать различными UNION'ами. Но этот вариант хорош, если мы можем выбрать одинаковые поля по каждой из сущностей. В рабочем проекте этого нам никто гарантировать не сможет. Значит, запрос к базе будем использовать только для получения данных, а все манипуляции с поиском будем выполнять с помощью PHP.

Настройка моделей ActiveRecord

Стремясь сохранить логику приложения, для выборки модели по условию поиска будем использовать отдельный метод AR-модели. Например, назовем его siteSearch. Он может выглядеть следующим образом:

public function siteSearch($text)
{
    $criteria=new CDbCriteria;
    $criteria->compare('field1',$text,true,'OR');
    $criteria->compare('field2',$text,true,'OR');
    $criteria->compare('field3',$text,true,'OR');
    return new CActiveDataProvider($this, array(
        'criteria'=>$criteria,
        'pagination' => false,
    ));
}
    

Для каждой ActiveRecord-модели, которая будет включена в поиск, необходимо создать аналогичный метод. С помощью него мы сможем получить массив всех данных, удовлетворяющих условиям поиска.

Создание экшна для реализации поиска по многим моделям

Основной задачей экшна является получение поискового запроса, создание коллекции моделей с данными, удовлетворяющими условиям поиска, и структуризация этих данных для удобного отображения в представлении. Рассмотрим вариант с двумя моделями News и StaticPages.

public function actionSearch($text, $page = 1)
{
    $output = 10;
    $pageSize = $output;
    $offset = ($page - 1)*$output;
    $news = News::model()->siteSearch($text);
    $staticPages = StaticPages::model()->siteSearch($text);
    $data = array($news, $staticPages);
    $count = 0;
    $j = 1;
    foreach ($data as $key => $material) {
        $count += $material->totalItemCount;
        $diff = $offset - $material->totalItemCount;
        if ($diff >= 0) {
            $data[$key] = '';
            $offset = $diff;
        } else {
            $skip = $offset;
            $offset = 0;
            //Deleting $skip items from beginning
            $tmp_arr = array();
            $tmp_arr = $material->getData();
            $res_arr = array();
            for ($i = $skip; $i<$material->totalItemCount; $i++) {
                $res_arr[] = $tmp_arr[$i];
            }
            $data[$key]->setData($res_arr);
            $data[$key]->setTotalItemCount(count($res_arr));
            $material->setData($res_arr);
            $d = $output - $material->totalItemCount;
            if ($d >= 0) {
                $output = $d;
            } else {
                //Delete all after $output items from $material
                $tmp_arr = $material->getData();
                $res_arr = array();
                for ($i = 0; $i<$output; $i++) {
                    $res_arr[] = $tmp_arr[$i];
                }
                $data[$key]->setData($res_arr);
                $output =0;
            }
        }
        $j++;
    }
    $news = $data[0];
    $staticPages = $data[1];
    $pages=new CPagination($count);
    // results per page
    $pages->pageSize=$pageSize;
    $pages->pageVar='page';
    $pages->params = array('text'=>$text);
    $pages->route = '/site/search';
    $this->render('search', array(
        'text'=>$text,
        'news'=>$news,
        'staticPages'=>$staticPages,
        'pages'=>$pages,
    ));
}

Итак, мы передаем в представление текст запроса, все модели по очереди и объект класса CPagination.

Оформление представления для результатов поиска

В представлении search блок вывода результатов поиска может выглядеть так:

<p>Результаты поиска для «<?php echo CHtml::encode($text); ?>»:</p>
<?php
    $arr = array('news', 'staticPages');
    foreach ($arr as $key => $item) {
        if (empty($item))
            unset($arr[$key]);
    }
    $arr = array_values($arr);
    foreach ($arr as $key => $controller) {
        $view_path = '/'.$controller.'/_search';
        $this->widget('zii.widgets.CListView', array(
            'dataProvider'=>$controller,
            'itemView'=>$view_path,
            'summaryText'=>false,
            'emptyText'=>false,
            'template'=>'{items}',
        ));
    }
?>
<?php $this->widget('CLinkPager', array(
    'pages' => $pages
)) ?>

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

Представление /news/_search для модели News может выглядеть примерно так:

<div class="news_preview">
    <?php echo CHtml::link($data->title, array('/news/view', 'id'=>$data->id)); ?>
    <p><?php echo strip_tags($data->annotation); ?></p>
</div>

Подбиваем результаты

Что же мы имеем в итоге, используя такой подход? Из недостатков метода:

  • необходимость вносить правки в экшн контроллера и представление search при добавлении новых типов материалов
  • при больших объемах данных запросы к моделям будут передавать все данные, и возникнет необходимость расчета ограничений

Из преимуществ подхода можно отметить:

  • универсальность в работе с разными структурами данных
  • отдельные представления для каждого типа материала
  • возможность сортировки выборки (может быть неэффективно при больших объемах данных)

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