Minggu, Agustus 31, 2008

Bikin Search Engine FullText dengan Zend Search Lucene - Searching/Pencarian

Pada posting sebelumnya saya sudah sedikit memaparkan bagaimana cara membuat dan menambahkan indeks database dokumen fulltext dengan menggunakan Zend Search Lucene. Sekarang saya akan sedikit memaparkan bagaimana cara untuk melakukan pencarian ke dalam indeks yang telah dibuat dengan menggunakan Zend Search Lucene.

Untuk melakukan pencarian, Zend Search Lucene menyediakan beberapa metode, tetapi yang paling simpel adalah menggunakan metode find(), dari objek INDEX (instance fungsi factory Zend_Search_Lucene::open). Metode find() mempunyai 2 argumen, argumen pertama adalah kata kunci/keyword yang ingin kita query, dan argumen kedua adalah default field metadata yang akan di-coba temukan oleh indexer. HATI-HATI DENGAN QUERY WILDCARD (*)! Listing programnya kira-kira seperti ini :

<?php
// include paging class
require 'lib/simbio_paging.inc.php';
require 'lib/utils.inc.php';
// get keywords
$keywords = trim($_GET['keywords']);
if ($keywords AND $keywords != '*') {
    // search the index
    $matches = $index->find($keywords, 'title');
    if ($num_matches = count($matches)) {
        // slice the array
        if ($num_matches > $config['recs_each_page']) {
            // get page number from http get var
            $page = 1;
            if (isset($_GET['page']) AND $_GET['page'] > 1) {
                $page = (integer)$_GET['page'];
            }
            // count the row offset
            $remove_offset = $config['recs_each_page'];
            if ($page > 1) {
                $remove_offset = ($page*$config['recs_each_page']);
                // slice from first element of array
                array_splice($matches, 0, ($remove_offset-$config['recs_each_page']));
            }
            // slice the rest elements of array
            array_splice($matches, $config['recs_each_page']);
        }
        echo 'Found <b>'.$num_matches.'</b> document matches your keyword : <hr size="1" />'."\n";
        foreach ($matches as $doc) {
            echo '<div style="clear: both: margin: 5px; margin-bottom: 20px;">'
                .'<div style="font-weight: bold;">'.$doc->title.'</div>'
                .'<div style="margin-left: 10px;">'.$doc->author.'</div>'
                .'<div style="margin-left: 10px;"><a href="?mod=search&action=action&docID='.urlencode($doc->checksum).'" target="_blank">'.basename($doc->file_name).'</a></div>'
                .'</div>';
        }
    }
    // paging
    if ($num_matches > $config['recs_each_page']) {
        echo simbio_paging::paging($num_matches, $config['recs_each_page'], 10);
    }
} else {
    echo utils::showError('No Keywords Entered!');
}
?>


Yang agak rumit mungkin adalah paging search resultnya. Zend Search Lucene kaga nyediain tuh yang namanya klausa "LIMIT", "OFFSET", "TOP" kaya di RDBMS-RDBMS populer, agak-agak tricky si caranya, tapi "it works". Ini saya langsung kasih juga script pagingnya yang saya ambil dari library development PHP, SIMBIO 2 yang selalu saya gunakan dalam mengembangkan aplikasi.

<?php
/**
 * simbio_paging
 * Paging Generator class
 *
 * Copyright (C) 2007,2008  Arie Nugraha (dicarve@yahoo.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
 
class simbio_paging
{
    /**
     * Static Method to print out the paging list
     *
     * @param   integer $int_all_recs_num
     * @param   integer $int_recs_each_page
     * @param   integer $int_pages_each_set
     * @param   string  $str_fragment
     * @param   string  $str_target_frame
     * @return  string
     */
    public static function paging($int_all_recs_num, $int_recs_each_page, $int_pages_each_set = 10, $str_fragment = '', $str_target_frame = '_self')
    {
        // check for wrong arguments
        if ($int_recs_each_page > $int_all_recs_num) {
            return;
        }
 
        // total number of pages
        $_num_page_total = ceil($int_all_recs_num/$int_recs_each_page);
 
        if ($_num_page_total < 2) {
            return;
        }
 
        // total number of pager set
        $_pager_set_num = ceil($_num_page_total/$int_pages_each_set);
 
        // check the current page number
        if (isset($_GET['page']) AND $_GET['page'] > 1) {
            $_page = (integer)$_GET['page'];
        } else {$_page = 1;}
 
        // check the query string
        if (isset($_SERVER['QUERY_STRING']) AND !empty($_SERVER['QUERY_STRING'])) {
            parse_str($_SERVER['QUERY_STRING'], $arr_query_var);
            // rebuild query str without "page" var
            $_query_str_page = '';
            foreach ($arr_query_var as $varname => $varvalue) {
                $varvalue = urlencode($varvalue);
                if ($varname != 'page') {
                    $_query_str_page .= $varname.'='.$varvalue.'&';
                }
            }
            // append "page" var at the end
            $_query_str_page .= 'page=';
            // create full URL
            $_current_page = $_SERVER['PHP_SELF'].'?'.$_query_str_page;
        } else {
            $_current_page = $_SERVER['PHP_SELF'].'?page=';
        }
 
        // target frame
        $str_target_frame = 'target="'.$str_target_frame.'"';
 
        // init the return string
        $_buffer = '<span class="pagingList">';
        $_stopper = 1;
 
        // count the offset of paging
        if (($_page > 5) AND ($_page%5 == 1)) {
            $_lowest = $_page-5;
            if ($_page == $_lowest) {
                $_pager_offset = $_lowest;
            } else {
                $_pager_offset = $_page;
            }
        } else if (($_page > 5) AND (($_page*2)%5 == 0)) {
            $_lowest = $_page-5;
            $_pager_offset = $_lowest+1;
        } else if (($_page > 5) AND ($_page%5 > 1)) {
            $_rest = $_page%5;
            $_pager_offset = $_page-($_rest-1);
        } else {
            $_pager_offset = 1;
        }
 
        // Previous page link
        if (defined('lang_sys_common_paging_first')) {
            $_first = lang_sys_common_paging_first;
        } else {
            $_first = 'First Page';
        }
 
        if (defined('lang_sys_common_paging_prev')) {
            $_prev = lang_sys_common_paging_prev;
        } else {
            $_prev = 'Previous Page';
        }
 
        if ($_page > 1) {
            $_buffer .= ' &nbsp;';
            $_buffer .= '<a href="'.$_current_page.(1).$str_fragment.'" '.$str_target_frame.'>'.$_first.'</a>&nbsp; '."\n";
            $_buffer .= ' &nbsp;';
            $_buffer .= '<a href="'.$_current_page.($_page-1).$str_fragment.'" '.$str_target_frame.'>'.$_prev.'</a>&nbsp; '."\n";
        }
 
        for ($p = $_pager_offset; ($p <= $_num_page_total) AND ($_stopper < $int_pages_each_set+1); $p++) {
            if ($p == $_page) {
                $_buffer .= ' &nbsp;<b>'.$p.'</b>&nbsp; '."\n";
            } else {
                $_buffer .= ' &nbsp;';
                $_buffer .= '<a href="'.$_current_page.$p.$str_fragment.'" '.$str_target_frame.'>'.$p.'</a>&nbsp; '."\n";
            }
 
            $_stopper++;
        }
 
        // Next page link
        if (defined('lang_sys_common_paging_next')) {
            $_next = lang_sys_common_paging_next;
        } else {
            $_next = 'Next';
        }
 
        if (($_pager_offset != $_num_page_total-4) AND ($_page != $_num_page_total)) {
            $_buffer .= ' &nbsp;';
            $_buffer .= '<a href="'.$_current_page.($_page+1).$str_fragment.'" '.$str_target_frame.'>'.$_next.'</a>&nbsp; '."\n";
        }
 
        // Last page link
        if (defined('lang_sys_common_paging_last')) {
            $_last = lang_sys_common_paging_last;
        } else {
            $_last = 'Last Page';
        }
 
        if ($_page < $_num_page_total) {
            $_buffer .= ' &nbsp;';
            $_buffer .= '<a href="'.$_current_page.($_num_page_total).$str_fragment.'" '.$str_target_frame.'>'.$_last.'</a>&nbsp; '."\n";
        }
 
        $_buffer .= '</span>';
 
        return $_buffer;
    }
}
?>

Nah begitulah kira-kira sedikit mengenai penggunaan Zend Search Lucene untuk membuat indeks dokumen fulltext. Dari sini kita bisa saja kembangkan untuk membuat search engine kecil-kecilan yang bermanfaat buat kita, contohnya saya meng-indeks manual PHP offline HTML agar saya bisa dengan cepat menemukan topik yang saya ingin baca.

Untuk peng-indeksan skala besar (jumlah dokumen dalam ukuran giga atau tera), saya menyarankan untuk menggunakan engine-engine indexing yang sudah mumpuni seperti Lucene (java), Clucene (C++), Swish-e, Lemur, Terrier, Xapian dll. Semoga artikel ini bermanfaat untuk anda yang membacanya.

2 komentar:

cuty mengatakan...

makasih sarannya...

Dian Masniari mengatakan...

Postingannya keren Banget Mas Ari..