27.7. Расширяемость

27.7.1. Анализ текста

Класс Zend_Search_Lucene_Analysis_Analyzer используется индексатором для разбиения текстовых полей документа на лексемы.

Методы Zend_Search_Lucene_Analysis_Analyzer::getDefault() и Zend_Search_Lucene_Analysis_Analyzer::setDefault() используются для получения и установки анализатора по умолчанию.

Таким образом, вы можете устанавливать собственный анализатор текста или выбирать его из ряда предопределенных анализаторов: Zend_Search_Lucene_Analysis_Analyzer_Common_Text и Zend_Search_Lucene_Analysis_Analyzer_Common_Text_CaseInsensitive (по умолчанию). Оба интерпретируют лексему как последовательность букв. Zend_Search_Lucene_Analysis_Analyzer_Common_Text_CaseInsensitive приводит лексемы к нижнему регистру.

Переключение между анализаторами:

<?php
Zend_Search_Lucene_Analysis_Analyzer::setDefault(
    new Zend_Search_Lucene_Analysis_Analyzer_Common_Text());
...
$index->addDocument($doc);

Zend_Search_Lucene_Analysis_Analyzer_Common спроектирован для того, чтобы быть родительским классом для всех анализаторов, определяемых пользователем. В наследующем классе достаточно определить методы reset() и nextToken(), которые берут строку из свойства $_input и возвращают лексемы шаг за шагом (null означает конец потока).

Метод nextToken() должен использовать метод normalize() для каждой лексемы. Это позволит использовать фильтры лексем с вашим анализатором.

Ниже приведен пример пользовательского анализатора, котрорый принимает слова с цифрами как элементы:

Пример 27.1. Собственный анализатор текста

<?php
/** Это созданный пользователем текстовый анализатор, который интерпретирует слова с цифрами как один элемент. */

/** Zend_Search_Lucene_Analysis_Analyzer hierarchy */
require_once 'Zend/Search/Lucene/Analysis/Analyzer.php';

class My_Analyzer extends Zend_Search_Lucene_Analysis_Analyzer_Common
{
    private $_position;

    /**
     * Установка позиции в начальное состояние
     */
    public function reset()
    {
        $this->_position = 0;
    }

    /**
     * API для разбиения на лексемы
     * Получение следующей лексемы
     * Возвращает null, если достигнут конец потока
     *
     * @return Zend_Search_Lucene_Analysis_Token|null
     */
    public function nextToken()
    {
        if ($this->_input === null) {
            return null;
        }

        while ($this->_position < strlen($this->_input)) {
            // пропуск пробела
            while ($this->_position < strlen($this->_input) &&
                   !ctype_alnum( $this->_input[$this->_position] )) {
                $this->_position++;
            }

            $termStartPosition = $this->_position;

            // чтение лексемы
            while ($this->_position < strlen($this->_input) &&
                   ctype_alnum( $this->_input[$this->_position] )) {
                $this->_position++;
            }

            // Пустая лексема, конец потока
            if ($this->_position == $termStartPosition) {
                return null;
            }

            $token = new Zend_Search_Lucene_Analysis_Token(
                                      substr($this->_input,
                                             $termStartPosition,
                                             $this->_position - $termStartPosition),
                                      $termStartPosition,
                                      $this->_position);
            $token = $this->normalize($token);
            if ($token !== null) {
                return $token;
            }
            // Продолжение, если лексема пропущена
        }

        return null;
    }
}

Zend_Search_Lucene_Analysis_Analyzer::setDefault(
    new My_Analyzer());

27.7.2. Фильтрация лексем

Анализатор Zend_Search_Lucene_Analysis_Analyzer_Common также предоставляет механизм фильтрации лексем.

Класс Zend_Search_Lucene_Analysis_TokenFilter является уровнем абстракции для таких фильтров. Он должен использоваться как предок для ваших собственных фильтров.

Пользовательские фильтры должны реализовать метод normalize(), который может преобразовывать лексему или сигнализировать, что лексема должна быть пропущена.

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

  • Zend_Search_Lucene_Analysis_TokenFilter_LowerCase

  • Zend_Search_Lucene_Analysis_TokenFilter_ShortWords

  • Zend_Search_Lucene_Analysis_TokenFilter_StopWords

Фильтр LowerCase уже используется для анализатора Zend_Search_Lucene_Analysis_Analyzer_Common_Text_CaseInsensitive, который применяется по умолчанию.

ShortWords и StopWords могут использоваться с уже включенными анализаторами или вашими собственными:

<?php
$stopWords = array('a', 'an', 'at', 'the', 'and', 'or', 'is', 'am');
$stopWordsFilter = new Zend_Search_Lucene_Analysis_TokenFilter_StopWords($stopWords);

$analyzer = new Zend_Search_Lucene_Analysis_Analyzer_Common_TextNum_CaseInsensitive();
$analyzer->addFilter($stopWordsFilter);

Zend_Search_Lucene_Analysis_Analyzer::setDefault($analyzer);
<?php
$shortWordsFilter = new Zend_Search_Lucene_Analysis_TokenFilter_ShortWords();

$analyzer = new Zend_Search_Lucene_Analysis_Analyzer_Common_TextNum_CaseInsensitive();
$analyzer->addFilter($shortWordsFilter);

Zend_Search_Lucene_Analysis_Analyzer::setDefault($analyzer);

Конструктор Zend_Search_Lucene_Analysis_TokenFilter_StopWords принимает массив стоп-слов в качестве аргумента. Но стоп-слова можно также загружать и из файла:

<?php
$stopWordsFilter = new Zend_Search_Lucene_Analysis_TokenFilter_StopWords();
$stopWordsFilter->loadFromFile($my_stopwords_file);

$analyzer = new Zend_Search_Lucene_Analysis_Analyzer_Common_TextNum_CaseInsensitive();
$analyzer->addFilter($stopWordsFilter);

Zend_Search_Lucene_Analysis_Analyzer::setDefault($analyzer);

Файл должен быть текстовым с одним словом в каждой строке. Символом '#' помечаются строки с комментариями.

Конструктор Zend_Search_Lucene_Analysis_TokenFilter_ShortWords имеет один необязательный параметр, это ограничение длины слова. Его значение по умолчанию равно 2.

27.7.3. Алгоритмы ранжирования

Ранг q документа d определяется следующим образом:

score(q,d) = sum( tf(t in d) * idf(t) * getBoost(t.field in d) * lengthNorm(t.field in d) ) * coord(q,d) * queryNorm(q)

tf(t in d) - Zend_Search_Lucene_Search_Similarity::tf($freq) - коэффициент ранга, основанный на том, насколько часто встречается элемент или фраза в документе.

idf(t) - Zend_Search_Lucene_Search_SimilaritySimilarity::tf($term, $reader) - коэффициент ранга для простого элемента применительно к определенному индексу.

getBoost(t.field in d) - коэффициент усиления для поля элемента.

lengthNorm($term) - значение нормализации для поля, получаемое из общего количества элементов, содержащихся в поле. Это значение хранится внутри индекса. Эти значения вместе с коэффициентом усиления поля хранятся в индексе, результатом их умножения является ранг для каждого поля.

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

сoord(q,d) - Zend_Search_Lucene_Search_Similarity::coord($overlap, $maxOverlap) - коэффициент ранга, основанный на относительной доле всех элементов запроса, найденных в документе.

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

queryNorm(q) - значение нормализации для запроса, получаемое из суммы возведенных в квадрат весов каждого из элементов запроса. Это значение затем умножается в вес каждого элемента запроса.

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

Алгоритм ранжирования может быть изменен через определение своего собственного класса. Для этого надо создать потомка класса Zend_Search_Lucene_Search_Similarity, как показано ниже, затем использовать метод Zend_Search_Lucene_Search_Similarity::setDefault($similarity); для установки объекта как используемого по умолчанию.

<?php

class MySimilarity extends Zend_Search_Lucene_Search_Similarity {
    public function lengthNorm($fieldName, $numTerms) {
        return 1.0/sqrt($numTerms);
    }

    public function queryNorm($sumOfSquaredWeights) {
        return 1.0/sqrt($sumOfSquaredWeights);
    }

    public function tf($freq) {
        return sqrt($freq);
    }

    /**
     * Сейчас не используется. Подсчитывает сумму соответствий неточной фразе,
     * основанную на изменяемом расстоянии.
     */
    public function sloppyFreq($distance) {
        return 1.0;
    }

    public function idfFreq($docFreq, $numDocs) {
        return log($numDocs/(float)($docFreq+1)) + 1.0;
    }

    public function coord($overlap, $maxOverlap) {
        return $overlap/(float)$maxOverlap;
    }
}

$mySimilarity = new MySimilarity();
Zend_Search_Lucene_Search_Similarity::setDefault($mySimilarity);

?>  

27.7.4. Контейнеры хранения

Абстрактный класс Zend_Search_Lucene_Storage_Directory определяет функционал директории.

Конструктор Zend_Search_Lucene использует строку или объект Zend_Search_Lucene_Storage_Directory как входные данные.

Zend_Search_Lucene_Storage_Directory_Filesystem реализует функционал директории для файловой системы.

Если для конструктора Zend_Search_Lucene в качестве входных данных испольуется строка, то считыватель индекса (объект Zend_Search_Lucene) рассматривает ее как путь в файловой системе и сама инстанцирует объекты Zend_Search_Lucene_Storage_Directory_Filesystem.

Вы можете определить собственную реализацию директории, создав потомка класса Zend_Search_Lucene_Storage_Directory.

Методы Zend_Search_Lucene_Storage_Directory:

<?php

abstract class Zend_Search_Lucene_Storage_Directory {
/**
 * Закрывает средство хранения.
 *
 * @return void
 */
abstract function close();


/**
 * Создает новый пустой файл с данным именем в директории.
 *
 * @param string $name
 * @return void
 */
abstract function createFile($filename);


/**
 * Удаляет существующий файл в директории.
 *
 * @param string $filename
 * @return void
 */
abstract function deleteFile($filename);


/**
 * Возвращает true, если файл с данным именем существует.
 *
 * @param string $filename
 * @return boolean
 */
abstract function fileExists($filename);


/**
 * Возвращает длину файла в директории.
 *
 * @param string $filename
 * @return integer
 */
abstract function fileLength($filename);


/**
 * Возвращает время последнего изменения файла в формате UNIX.
 *
 * @param string $filename
 * @return integer
 */
abstract function fileModified($filename);


/**
 * Переименовывает существующий файл в директории.
 *
 * @param string $from
 * @param string $to
 * @return void
 */
abstract function renameFile($from, $to);


/**
 * Устанавливает время изменения файла в текущее.
 *
 * @param string $filename
 * @return void
 */
abstract function touchFile($filename);


/**
 * Возвращает объект Zend_Search_Lucene_Storage_File для данного файла в директории.
 *
 * @param string $filename
 * @return Zend_Search_Lucene_Storage_File
 */
abstract function getFileObject($filename);

}

?>

Метод getFileObject($filename) класса Zend_Search_Lucene_Storage_Directory возвращает объект Zend_Search_Lucene_Storage_File.

Абстрактный класс Zend_Search_Lucene_Storage_File реализует абстракцию файла и примитивы чтения файла индекса.

Вы должны создать класс, наследующий от Zend_Search_Lucene_Storage_File для своей реализации директории.

Только два метода класса Zend_Search_Lucene_Storage_File должны быть перегружены в вашей реализации:

<?php

class MyFile extends Zend_Search_Lucene_Storage_File {
    /**
     * Устанавливает индикатор позиции и перемещает указатель файла.
     * Новая позиция, измеряемая в байтах от начала файла,
     * получается добавлением смещения к позиции, определяемой аргументом $whence,
     * который может принимать следующие значения:
     * SEEK_SET - Устанавливает позицию равной смещению в байтах.
     * SEEK_CUR - Устанавливает позицию равной текущей позиции плюс смещение.
     * SEEK_END - Устанавливает позицию равной концу файла плюс смещение.
     * (Для перемещения позиции относительно конца файла вы должны передать отрицательное значение смещения)
     * В случае успеха возвращает 0; иначе -1
     *
     * @param integer $offset
     * @param integer $whence
     * @return integer
     */
    public function seek($offset, $whence=SEEK_SET) {
        ...
    }

    /**
     * Считывает $length байт из файла и перемещает указатель файла.
     *
     * @param integer $length
     * @return string
     */
    protected function _fread($length=1) {
        ...
    }
}

?>