Zend Lucene和PDF文档第3部分:为文档建立索引

上一次我们达到了拥有PDF元数据和PDF文档的提取内容准备好进入我们的搜索索引类以便我们对其进行搜索的阶段。

首先需要设置几个配置选项。这将控制我们的Lucene索引和要建立索引的PDF文件的保留位置。将以下选项添加到配置文件中(application.ini如果您使用Zend Tool创建应用程序则调用)。

luceneIndex = \path\to\lucene\index
filesDirectory = \path\to\pdf\files\

您可以通过在Bootstrap.php文件中使用以下方法将这些选项加载到Zend_Registry中。

protected function _initConfigLoad()
{
    $config = new Zend_Config_Ini(APPLICATION_PATH . '/configs/application.ini', APPLICATION_ENV);
    Zend_Registry::set('config', $config);
}

应该在索引文件中定义APPLICATION_ENV常量。

接下来,我们需要一种使我们的应用程序对所有PDF文件建立索引的方法,为此,我在Index conrtoller中创建了一个名为的操作indexpdfAction()。此功能将执行的操作是遍历我们PDF文件夹中的每个文件,并将其添加到我们的Lucene索引中。索引编制完成后,该操作将把索引中的文档数和索引大小发送到视图,以便我们可以查看对多少文件进行了索引。以下代码块包含该操作的完整源代码。请注意,此代码将不会运行,因为其背后的大多数代码尚不存在。

public function indexpdfsAction() {
    $config = Zend_Registry::get('config');
    $appLucene = App_Search_Lucene::open($config->luceneIndex);
    $globOut = glob($config->filesDirectory . '*.pdf');
    if (count($globOut) > 0) { // 确保glob数组中有东西
        foreach ($globOut as $filename) {
            $index = App_Search_Lucene_Index_Pdfs::index($filename, $appLucene);
        }
    }
    $appLucene->commit();
    if ($appLucene != null) {
        $this->view->indexSize = $appLucene->count();
        $this->view->documents = $appLucene->numDocs();
    }
}

该代码的第一行涉及从注册表获取我们的配置对象,以便我们可以使用它来找到Lucene索引和PDF文档在文件系统上的位置。

下一行调用open()名为App_Search_Lucene的类的静态方法,并传递我们的Lucene索引的位置。此类扩展了Zend_Search_Lucene对象,以便在从索引中添加和删除文档以及首先创建索引时,我们可以具有额外的控制级别。我们正在使用扩展名来控制如何将文档添加到索引中,因为无法通过Zend_Search_Lucene更新文档,我们必须先删除文档,然后再将其重新添加到索引中。

该open()方法尝试使用称为Zend_Search_Lucene_Proxy的类打开Lucene索引,如果该索引尚不存在,则使用该create()方法创建该索引。Zend_Search_Lucene_Proxy对象用于在我们的应用程序和Zend_Search_Lucene对象中可用的方法之间提供网关,但仍然允许我们控制如何将文件添加到索引。

class App_Search_Lucene extends Zend_Search_Lucene
{
    /**
     * Create a new index.
     *
     * @param string $directory         The location of the Lucene index.
     * @return Zend_Search_Lucene_Proxy The Lucene index.
     */
    public static function create($directory)
    {
        return new Zend_Search_Lucene_Proxy(new App_Search_Lucene($directory, true));
    }
 
    /**
     * Open the index. If the index does not exist then one will be created and
     * returned.
     *
     * @param string $directory         The location of the Lucene index.
     * @return Zend_Search_Lucene_Proxy The Lucene index.
     */
    public static function open($directory)
    {
        try {
            // 尝试打开索引。
            return new Zend_Search_Lucene_Proxy(new App_Search_Lucene($directory, false));
        } catch (Exception $e) {
            // 使用此类的create方法返回新创建的索引。
            return self::create($directory);
        }
    }
 
    /**
     * Add a document to the index.
     *
     * @param Zend_Search_Lucene_Document $document The document to be added.
     * @return Zend_Search_Lucene                   
     */
    public function addDocument(Zend_Search_Lucene_Document $document)
    {
        // 搜索具有相同键值的文档。
        $term = new Zend_Search_Lucene_Index_Term($document->Key, 'Key');
        $docIds = $this->termDocs($term);
 
        // 删除找到的所有文档。
        foreach ($docIds as $id) {
            $this->delete($id);
        }
 
        return parent::addDocument($document);
    }
}

我们在此应用程序中使用的第二个对象称为App_Search_Lucene_Index_Pdfs。这有一个称为的方法index(),该方法带有两个参数。第一个是PDF文档的路径,第二个是Lucene索引。此方法的作用是查找尽可能多的有关PDF文档的信息,然后将该文档添加到Lucene索引中。最后一步是创建一个名为App_Search_Lucene_Document的对象的实例(在后面的内容中进行说明),并将其发送到addDocument()App_Search_Lucene对象的方法。在该类中,前两篇文章中的所有代码都将发挥作用。打开PDF文档后,对象将元数据读取到数组中,然后提取PDF的文本内容并将其添加到数组中。此类是相当自我解释的,并且使用您在本系列文章中已经看到的代码,因此我将不对其进行详细介绍。我在关键点处的代码中添加了注释,以解释发生了什么。

class App_Search_Lucene_Index_Pdfs
{
    /**
     * Extract data from a PDF document and add this to the Lucene index.
     *
     * @param string $pdfPath                       The path to the PDF document.
     * @param Zend_Search_Lucene_Proxy $luceneIndex The Lucene index object.
     * @return Zend_Search_Lucene_Proxy
     */
    public static function index($pdfPath, $luceneIndex)
    {
        // 加载PDF文档。
        $pdf = Zend_Pdf::load($pdfPath);
        $key = md5($pdfPath);
 
        /**
         * Set up array to contain the document index data.
         * The Filename will be used to retrive the document if it is found in
         * the search resutls.
         * The Key will be used to uniquely identify the document so we can
         * delete it from the search index when adding it.
         */
        $indexValues = array(
            'Filename'     => $pdfPath,
            'Key'          => $key,
            'Title'        => '',
            'Author'       => '',
            'Subject'      => '',
            'Keywords'     => '',
            'Creator'      => '',
            'Producer'     => '',
            'CreationDate' => '',
            'ModDate'      => '',
            'Contents'     => '',
        );
 
        // 遍历每个元数据项并添加到索引数组。
        foreach ($pdf->properties as $meta => $metaValue) {
            switch ($meta) {
                case 'Title':
                    $indexValues['Title'] = $pdf->properties['Title'];
                    break;
                case 'Subject':
                    $indexValues['Subject'] = $pdf->properties['Subject'];
                    break;
                case 'Author':
                    $indexValues['Author'] = $pdf->properties['Author'];
                    break;
                case 'Keywords':
                    $indexValues['Keywords'] = $pdf->properties['Keywords'];
                    break;
                case 'CreationDate':
                    $dateCreated = $pdf->properties['CreationDate'];
 
                    $distance = substr($dateCreated, 16, 2);
                    if (!is_long($distance)) {
                        $distance = null;
                    }
                    // 从D:20090731160351 + 01'00'的PDF格式转换日期
                    $dateCreated = mktime(substr($dateCreated, 10, 2), //hour
                        substr($dateCreated, 12, 2), //minute
                        substr($dateCreated, 14, 2), //second
                        substr($dateCreated,  6, 2), //month
                        substr($dateCreated,  8, 2), //day
                        substr($dateCreated,  2, 4), //year
                        $distance); //distance
                    $indexValues['CreationDate'] = $dateCreated;
                    break;
                case 'Date':
                    $indexValues['Date'] = $pdf->properties['Date'];
                    break;
            }
        }
 
        /**
         * Parse the contents of the PDF document and pass the text to the
         * contents item in the $indexValues array.
         */
        $pdfParse                = new App_Search_Helper_PdfParser();
        $indexValues['Contents'] = $pdfParse->pdf2txt($pdf->render());
 
        // 使用值创建文档
        $doc = new App_Search_Lucene_Document($indexValues);
        if ($doc !== false) {
            // 如果文档创建成功,则将其添加到我们的索引中。
            $luceneIndex->addDocument($doc);
        }
 
        // 返回Lucene索引对象。
        return $luceneIndex;
    }
}

Key属性用于快速轻松地唯一标识索引中的文件。对于此类,我将其设置为文件名的md5,但是可以将其更改为其他名称。

App_Search_Lucene_Document类是最需要解释的一类,主要是由于创建它时所做出的决定。该类扩展了Zend_Search_Lucene_Document类,因此其行为就像普通的Lucene文档一样。向构造函数传递一个包含要写入文档的值的关联数组的单个参数。然后,此类的其余部分将数组的各项作为字段对象添加到文档中。以下代码块包含此类的源代码。

class App_Search_Lucene_Document extends Zend_Search_Lucene_Document
{
 
    /**
     * Constructor.
     *
     * @param array $values An associative array of values to be used
     *                      in the document.
     */
    public function __construct($values)
    {
        // 如果未设置“文件名”或“密钥”值,则拒绝该文档。
        if (!isset($values['Filename']) && !isset($values['key'])) {
            return false;
        }
 
        // 将“文件名”字段作为“关键字”字段添加到文档中。
        $this->addField(Zend_Search_Lucene_Field::Keyword('Filename', $values['Filename']));
        // 将“关键字”字段作为关键字添加到文档中。
        $this->addField(Zend_Search_Lucene_Field::Keyword('Key', $values['Key']));
 
        if (isset($values['Title']) && $values['Title'] != '') {
            // 将标题字段作为文本字段添加到文档中。
            $this->addField(Zend_Search_Lucene_Field::Text('Title', $values['Title']));
        }
 
        if (isset($values['Subject']) && $values['Subject'] != '') {
            // 将主题字段作为文本字段添加到文档中。
            $this->addField(Zend_Search_Lucene_Field::Text('Subject', $values['Subject']));
        }
 
        if (isset($values['Author']) && $values['Author'] != '') {
            // 将“作者”字段作为“文本”字段添加到文档中。
            $this->addField(Zend_Search_Lucene_Field::Text('Author', $values['Author']));
        }
 
        if (isset($values['Keywords']) && $values['Keywords'] != '') {
            // 将“关键字”字段作为“关键字”字段添加到文档中。
            $this->addField(Zend_Search_Lucene_Field::Keyword('Keywords', $values['Keywords']));
        }
 
        if (isset($values['CreationDate']) && $values['CreationDate'] != '') {
            // 将CreationDate字段作为文本字段添加到文档中。
            $this->addField(Zend_Search_Lucene_Field::Text('CreationDate', $values['CreationDate']));
        }
 
        if (isset($values['ModDate']) && $values['ModDate'] != '') {
            // 将ModDate字段作为文本字段添加到文档中。
            $this->addField(Zend_Search_Lucene_Field::Text('ModDate', $values['ModDate']));
        }
 
        if (isset($values['Contents']) && $values['Contents'] != '') {
            // 将目录字段作为未存储字段添加到文档中。
            $this->addField(Zend_Search_Lucene_Field::UnStored('Contents', $values['Contents']));
        }
    }
}

有五种不同类型的字段对象可用,每种对象以不同的方式起作用。我在此类中使用的三种字段类型是“关键字”,“文本”和“未存储”。这是每个选项的简短解释以及为什么选择它们。

  • 关键字-这些字段已存储并建立索引,因此在搜索时可用。但是,没有对字符串进行任何处理或标记化,因此整个字符串都按原样存储。我为“文件名”和“密钥”字段选择了此选项,因为它最适合完整搜索的字符串

  • 文本-文本字段被存储,索引和标记化。此类中的大多数字段都存储为文本字段,因为它们不应太长,并且在需要时可以显示在搜索结果中。

  • 未保存-保存字段被当作只不过它们不是存储在索引中的文本字段。当处理潜在的大量文本时,这种类型的字段是理想的,因此以这种方式处理内容。与其占用磁盘空间并将整个文档存储在索引中,不如让Lucene对文本进行标记化以便可以对其进行搜索,这是最好的选择。使用“未存储”字段意味着我们无法在搜索结果中打印出该字段,但是这里没有任何必要,因为我们将拥有描述和提供指向我们PDF文档的链接所需的所有信息。

有关Zend_Search_Lucene中可用的不同字段类型的更多信息,请参见Zend Search Lucene字段类型文档。

如果不存在Filename或Key值,则App_Search_Lucene_Document类在构造函数中将返回false。可以在此类中创建Key,但是将App_Search_Lucene_Index_Pdfs类中的数据处理与App_Search_Lucene_Document中的文档创建完全分开是有意义的。

请记住,所有类名都取决于它们在我们应用程序的库文件夹中的位置。因此Pdfs.php,将调用类App_Search_Lucene_Index_Pdfs并将其位于\ App \ Search \ Lucene \ Index中。为了清楚起见,我已写出每个文件应位于的位置。

--application
--library
----App 
------Search
--------Helper
----------PdfParser.php
--------Lucene
----------Index
------------Pdfs.php
--------Document.php
------Lucene.php
----Zend

如果我们要向索引服务添加不同的文件类型,或者甚至更改为Xapian之类的其他搜索引擎,则此目录结构允许将来进行更改。

在下一期Zend Lucene和PDF文档中,我将向您展示如何向应用程序中添加搜索表单,以便我们可以搜索已建立索引的文档。我将在最后一集中提供所有源代码,因此如果您想掌握它,请保持发布状态。

  • Zend Lucene和PDF文档第1部分:PDF元数据

  • Zend Lucene和PDF文档第2部分:PDF数据提取

  • Zend Lucene和PDF文档第3部分:为文档建立索引

  • Zend Lucene和PDF文档第4部分:搜索

  • Zend Lucene和PDF文档第5部分:结论