Pull to refresh

Как подружить Yii (ActiveDataProvider) и Text Search в PostgreSQL

Reading time5 min
Views3.2K

Использование PostgreSQL tsearch2 в проекте на Yii


Любой сайт — это прежде всего тексты. Для того, чтобы тексты было удобно редактировать их часто хранят в БД. При этом появляются дополнительные возможности, такие как удобный поиск по содержимому текстового поля. Старый добрый LIKE хорош, но не всегда. Есть более продвинутые вещи, такие как tsearch2 в PostgreSQL. Как им воспользоваться в Yii Framework я расскажу под катом.

Преамбула


Однажды мне пришлось реализовывать полнотекстовый поиск на одном из сайтов, созданных мной для меня же. Для работы с tsearch2 не нужно долго гуглить и думать как оно работает, потому как на сайте есть исчерпывающее руководство. В чем прелесть использования tsearch2 по сравнению с LIKE думаю тоже объяснять не нужно. На этом сайте информации полно. Так вот, если в приложении писать SQL запросы самому в явном виде то проблем никаких, но я пользуюсь Yii и мне хотелось чтобы все было сделано путем, рекомендуемым разработчиками. Однако это не единственная причина. Для использования виджета CListView нам нужно подготовить экземпляр класса CActiveDataProvider. Вот тут то и началось самое интересное.

Решение


Для использования CActiveDataProvider нам нужно инициализировать объект класса CDbCriteria, однако в нем нет возможности изменить поле FROM для добавления туда вызова функции, формирующего запрос к движку tsearch2, вида:

SELECT ... FROM ..., to_tsquery($sh_string) AS q ...


Остается только использование модели, заполняемой вызовом методом findAllBySql, однако встроенный в CActiveDataProvider механизм поддерживает только findAll. Так как же нам сконвертировать модель, полученную из чистого SQL запроса, в CActiveDataProvider? Решение было найдено на Stackoverflow однако на этом неприятности не закончились. У меня, в таблице где хранились тексты, ключевое поле называлось не id, а txt_id, в связи с этим пришлось вносить в текст из ответа небольшую, но очень важную поправку. Вот что получилось у меня.
В контроллере:

$_shString = '';
if (isset($_GET['sh']))
{
    $_shString = implode('&', explode(' ', $_GET['sh']));
    $_qry = "
        SELECT
            txt_id,
            ts_headline(txt, q, 'StartSel=<strong>, StopSel=</strong>, MaxWords=35, MinWords=15') AS txt,
            ts_rank(fti_txt, q) AS rank
        FROM
            texts, to_tsquery(:sh) AS q
        WHERE
            user_id=:uid AND fti_txt @@ q
        ORDER BY rank DESC
    "
;
 
    $_model = Texts::model()->findAllBySql($_qry, array(':uid' => Yii::app()->user->id, ':sh' => $_shString));
}


В представлении:

$this->widget('zii.widgets.CListView', array(
    'id' => 'search-results',
    'dataProvider' => new CArrayDataProvider($model, array('keyField' => 'txt_id')),
    'itemView' => '_text',
));


Та самая правка относится к
array('keyField' => 'txt_id')


UPD. 2012-04-02


Как написал в каментах уважаемый Rive, проблема в том, что при таком подходе все результаты поиска будут выбираться как изначально, так и при переходе по страницам в CListView. В поисках решения мне пришлось задать вопрос на форуме фреймворка на что был незамедлительно получен короткий, но полностью исчерпывающий ответ! Кому лень читать скажу, что суть ответа: Используйте CSqlDataProvider

Мой код получился таким. В контроллере:
$_shString = '';
if (isset($_GET['sh']))
{
    $_shString = implode('&', explode(' ', $_GET['sh']));
 
    $_cnt = Yii::app()->db->createCommand('SELECT count(*) FROM texts, to_tsquery(:sh) AS q WHERE user_id=:uid AND fti_txt @@ q')->queryScalar(array(':uid' => Yii::app()->user->id, ':sh' => $_shString));
 
    $_qry = "
        SELECT
            txt_id,
            ts_headline(txt, q, 'StartSel=<strong>, StopSel=</strong>, MaxWords=35, MinWords=15') AS txt,
            ts_rank(fti_txt, q) AS rank
        FROM
            texts, to_tsquery(:sh) AS q
        WHERE
            user_id=:uid AND fti_txt @@ q
        ORDER BY rank DESC
    "
;
 
    $dataProvider = new CSqlDataProvider($_qry, array(
        'totalItemCount' => $_cnt,
        'params' => array(':uid' => Yii::app()->user->id, ':sh' => $_shString),
        'keyField' => 'txt_id',
        'pagination' => array(
            'pageSize' => 20,
        ),
    ));
}
 

Далее во view:
$this->widget('zii.widgets.CListView', array(
    'id' => 'search-results',
    'dataProvider' => $dataProvider,
    'itemView' => '_text',
));
 

Как видим, для прорисовки каждого элемента списка используется вспомогательный view _text.php, вот простейший вариант его содержимого:
<div class="view">
    <?php 
        //echo CHtml::encode($data->txt);
        //echo $data->txt;
        echo $data['txt'];
    ?>
</div>


Особо нужно обратить внимание на два момента:
  1. При создании экземпляра CSqlDataProvider надо отдельно находить и передавать ему общее количество записей, возвращаемое запросом. Получается, что первоначально выполняется два аналогичных по тяжести запроса. Это прописано в официальной документации
  2. Получаемый при «проходе» по результатам запроса компонент $data внутри провайдера имеет формат массива, а не объекта. Исходник _text.php я привел не зря, тут надо использовать echo $data['txt']; вместо echo $data->txt;

Надеюсь этот опыт будет кому-нибудь полезным!
Tags:
Hubs:
+3
Comments3

Articles

Change theme settings