I18N_Arabic
[ class tree: I18N_Arabic ] [ index: I18N_Arabic ] [ all elements ]

Source for file Query.php

Documentation is available at Query.php

  1. <?php
  2. /**
  3.  * ----------------------------------------------------------------------
  4.  *  
  5.  * Copyright (c) 2006-2013 Khaled Al-Sham'aa.
  6.  *  
  7.  * http://www.ar-php.org
  8.  *  
  9.  * PHP Version 5
  10.  *  
  11.  * ----------------------------------------------------------------------
  12.  *  
  13.  * LICENSE
  14.  *
  15.  * This program is open source product; you can redistribute it and/or
  16.  * modify it under the terms of the GNU Lesser General Public License (LGPL)
  17.  * as published by the Free Software Foundation; either version 3
  18.  * of the License, or (at your option) any later version.
  19.  *  
  20.  * This program is distributed in the hope that it will be useful,
  21.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  23.  * GNU Lesser General Public License for more details.
  24.  *  
  25.  * You should have received a copy of the GNU Lesser General Public License
  26.  * along with this program.  If not, see <http://www.gnu.org/licenses/lgpl.txt>.
  27.  *  
  28.  * ----------------------------------------------------------------------
  29.  *  
  30.  * Class Name: Arabic Queary Class
  31.  *  
  32.  * Filename: Query.php
  33.  *  
  34.  * Original  Author(s): Khaled Al-Sham'aa <khaled@ar-php.org>
  35.  *  
  36.  * Purpose:  Build WHERE condition for SQL statement using MySQL REGEXP and
  37.  *           Arabic lexical  rules
  38.  *            
  39.  * ----------------------------------------------------------------------
  40.  *  
  41.  * Arabic Queary Class
  42.  *
  43.  * PHP class build WHERE condition for SQL statement using MySQL REGEXP and
  44.  * Arabic lexical  rules.
  45.  *    
  46.  * With the exception of the Qur'an and pedagogical texts, Arabic is generally
  47.  * written without vowels or other graphic symbols that indicate how a word is
  48.  * pronounced. The reader is expected to fill these in from context. Some of the
  49.  * graphic symbols include sukuun, which is placed over a consonant to indicate that
  50.  * it is not followed by a vowel; shadda, written over a consonant to indicate it is
  51.  * doubled; and hamza, the sign of the glottal stop, which can be written above or
  52.  * below (alif) at the beginning of a word, or on (alif), (waaw), (yaa'),
  53.  * or by itself on the line elsewhere. Also, common spelling differences regularly
  54.  * appear, including the use of (haa') for (taa' marbuuta) and (alif maqsuura)
  55.  * for (yaa'). These features of written Arabic, which are also seen in Hebrew as
  56.  * well as other languages written with Arabic script (such as Farsi, Pashto, and
  57.  * Urdu), make analyzing and searching texts quite challenging. In addition, Arabic
  58.  * morphology and grammar are quite rich and present some unique issues for
  59.  * information retrieval applications.
  60.  * 
  61.  * There are essentially three ways to search an Arabic text with Arabic queries:
  62.  * literal, stem-based or root-based.
  63.  * 
  64.  * A literal search, the simplest search and retrieval method, matches documents
  65.  * based on the search terms exactly as the user entered them. The advantage of this
  66.  * technique is that the documents returned will without a doubt contain the exact
  67.  * term for which the user is looking. But this advantage is also the biggest
  68.  * disadvantage: many, if not most, of the documents containing the terms in
  69.  * different forms will be missed. Given the many ambiguities of written Arabic, the
  70.  * success rate of this method is quite low. For example, if the user searches
  71.  * for (kitaab, book), he or she will not find documents that only
  72.  * contain (`al-kitaabu, the book).
  73.  * 
  74.  * Stem-based searching, a more complicated method, requires some normalization of
  75.  * the original texts and the queries. This is done by removing the vowel signs,
  76.  * unifying the hamza forms and removing or standardizing the other signs.
  77.  * Additionally, grammatical affixes and other constructions which attach directly
  78.  * to words, such as conjunctions, prepositions, and the definite article, should be
  79.  * identified and removed. Finally, regular and irregular plural forms need to be
  80.  * identified and reduced to their singular forms. Performing this type of stemming
  81.  * leads to more successful searches, but can be problematic due to over-generation
  82.  * or incorrect generation of stems.
  83.  * 
  84.  * A third method for searching Arabic texts is to index and search for the root
  85.  * forms of each word. Since most verbs and nouns in Arabic are derived from
  86.  * triliteral (or, rarely, quadriliteral) roots, identifying the underlying root of
  87.  * each word theoretically retrieves most of the documents containing a given search
  88.  * term regardless of form. However, there are some significant challenges with this
  89.  * approach. Determining the root for a given word is extremely difficult, since it
  90.  * requires a detailed morphological, syntactic and semantic analysis of the text to
  91.  * fully disambiguate the root forms. The issue is complicated further by the fact
  92.  * that not all words are derived from roots. For example, loan words (words
  93.  * borrowed from another language) are not based on root forms, although there are
  94.  * even exceptions to this rule. For example, some loans that have a structure
  95.  * similar to triliteral roots, such as the English word film, are handled
  96.  * grammatically as if they were root-based, adding to the complexity of this type
  97.  * of search. Finally, the root can serve as the foundation for a wide variety of
  98.  * words with related meanings. The root (k-t-b) is used for many words related
  99.  * to writing, including (kataba, to write), (kitaab, book), (maktab,
  100.  * office), and (kaatib, author). But the same root is also used for regiment/
  101.  * battalion, (katiiba). As a result, searching based on root forms results in
  102.  * very high recall, but precision is usually quite low.
  103.  * 
  104.  * While search and retrieval of Arabic text will never be an easy task, relying on
  105.  * linguistic analysis tools and methods can help make the process more successful.
  106.  * Ultimately, the search method you choose should depend on how critical it is to
  107.  * retrieve every conceivable instance of a word or phrase and the resources you
  108.  * have to process search returns in order to determine their true relevance.
  109.  * 
  110.  * Source: Volume 13 Issue 7 of MultiLingual Computing &
  111.  * Technology published by MultiLingual Computing, Inc., 319 North First Ave.,
  112.  * Sandpoint, Idaho, USA, 208-263-8178, Fax: 208-263-6310.
  113.  * 
  114.  * Example:
  115.  * <code>
  116.  *     include('./I18N/Arabic.php');
  117.  *     $obj = new I18N_Arabic('Query');
  118.  *     
  119.  *     $dbuser = 'root';
  120.  *     $dbpwd = '';
  121.  *     $dbname = 'test';
  122.  *     
  123.  *     try {
  124.  *         $dbh = new PDO('mysql:host=localhost;dbname='.$dbname, $dbuser, $dbpwd);
  125.  * 
  126.  *         // Set the error reporting attribute
  127.  *         $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  128.  * 
  129.  *         $dbh->exec("SET NAMES 'utf8'");
  130.  *     
  131.  *         if ($_GET['keyword'] != '') {
  132.  *             $keyword = @$_GET['keyword'];
  133.  *             $keyword = str_replace('\"', '"', $keyword);
  134.  *     
  135.  *             $obj->setStrFields('headline');
  136.  *             $obj->setMode($_GET['mode']);
  137.  *     
  138.  *             $strCondition = $Arabic->getWhereCondition($keyword);
  139.  *         } else {
  140.  *             $strCondition = '1';
  141.  *         }
  142.  *     
  143.  *         $StrSQL = "SELECT `headline` FROM `aljazeera` WHERE $strCondition";
  144.  * 
  145.  *         $i = 0;
  146.  *         foreach ($dbh->query($StrSQL) as $row) {
  147.  *             $headline = $row['headline'];
  148.  *             $i++;
  149.  *             if ($i % 2 == 0) {
  150.  *                 $bg = "#f0f0f0";
  151.  *             } else {
  152.  *                 $bg = "#ffffff";
  153.  *             }
  154.  *             echo "<tr bgcolor=\"$bg\"><td>$headline</td></tr>";
  155.  *         }
  156.  * 
  157.  *         // Close the databse connection
  158.  *         $dbh = null;
  159.  * 
  160.  *     } catch (PDOException $e) {
  161.  *         echo $e->getMessage();
  162.  *     }
  163.  * </code>
  164.  * 
  165.  * @category  I18N
  166.  * @package   I18N_Arabic
  167.  * @author    Khaled Al-Sham'aa <khaled@ar-php.org>
  168.  * @copyright 2006-2013 Khaled Al-Sham'aa
  169.  *    
  170.  * @license   LGPL <http://www.gnu.org/licenses/lgpl.txt>
  171.  * @link      http://www.ar-php.org
  172.  */
  173.  
  174. // New in PHP V5.3: Namespaces
  175. // namespace I18N\Arabic;
  176. // 
  177. // $obj = new I18N\Arabic\Query();
  178. // 
  179. // use I18N\Arabic;
  180. // $obj = new Arabic\Query();
  181. //
  182. // use I18N\Arabic\Query as Query;
  183. // $obj = new Query();
  184.  
  185. /**
  186.  * This PHP class build WHERE condition for SQL statement using MySQL REGEXP and
  187.  * Arabic lexical  rules
  188.  *  
  189.  * @category  I18N
  190.  * @package   I18N_Arabic
  191.  * @author    Khaled Al-Sham'aa <khaled@ar-php.org>
  192.  * @copyright 2006-2013 Khaled Al-Sham'aa
  193.  *    
  194.  * @license   LGPL <http://www.gnu.org/licenses/lgpl.txt>
  195.  * @link      http://www.ar-php.org
  196.  */ 
  197. {
  198.     private $_fields          array();
  199.     private $_lexPatterns     array();
  200.     private $_lexReplacements array();
  201.  
  202.     /**
  203.      * Loads initialize values
  204.      */         
  205.     public function __construct()
  206.     {
  207.         $xml simplexml_load_file(dirname(__FILE__).'/data/ArQuery.xml')
  208.          
  209.         foreach ($xml->xpath("//preg_replace[@function='__construct']/pair")
  210.                  as $pair
  211.  
  212.                  array_push($this->_lexPatterns(string)$pair->search)
  213.             array_push($this->_lexReplacements(string)$pair->replace)
  214.         }
  215.     }
  216.     
  217.     /**
  218.      * Setting value for $_fields array
  219.      *      
  220.      * @param array $arrConfig Name of the fields that SQL statement will search
  221.      *                          them (in array format where items are those
  222.      *                          fields names)
  223.      *                       
  224.      * @return object $this to build a fluent interface
  225.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  226.      */
  227.     public function setArrFields($arrConfig)
  228.     {
  229.         if (is_array($arrConfig)) {
  230.             // Get _fields array
  231.             $this->_fields $arrConfig;
  232.         }
  233.         
  234.         return $this;
  235.     }
  236.     
  237.     /**
  238.      * Setting value for $_fields array
  239.      *      
  240.      * @param string $strConfig Name of the fields that SQL statement will search
  241.      *                           them (in string format using comma as delimated)
  242.      *                          
  243.      * @return object $this to build a fluent interface
  244.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  245.      */
  246.     public function setStrFields($strConfig)
  247.     {
  248.         if (is_string($strConfig)) {
  249.             // Get _fields array
  250.             $this->_fields explode(','$strConfig);
  251.         }
  252.  
  253.         return $this;
  254.     }
  255.     
  256.     /**
  257.      * Setting $mode propority value that refer to search mode
  258.      * [0 for OR logic | 1 for AND logic]
  259.      *      
  260.      * @param integer $mode Setting value to be saved in the $mode propority
  261.      *      
  262.      * @return object $this to build a fluent interface
  263.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  264.      */
  265.     public function setMode($mode)
  266.     {
  267.         if (in_array($modearray('0''1'))) {
  268.             // Set search mode [0 for OR logic | 1 for AND logic]
  269.             $this->mode $mode;
  270.         }
  271.         
  272.         return $this;
  273.     }
  274.     
  275.     /**
  276.      * Getting $mode propority value that refer to search mode
  277.      * [0 for OR logic | 1 for AND logic]
  278.      *      
  279.      * @return integer Value of $mode properity
  280.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  281.      */
  282.     public function getMode()
  283.     {
  284.         // Get search mode value [0 for OR logic | 1 for AND logic]
  285.         return $this->mode;
  286.     }
  287.     
  288.     /**
  289.      * Getting values of $_fields Array in array format
  290.      *      
  291.      * @return array Value of $_fields array in Array format
  292.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  293.      */
  294.     public function getArrFields()
  295.     {
  296.         $fields $this->_fields;
  297.         
  298.         return $fields;
  299.     }
  300.     
  301.     /**
  302.      * Getting values of $_fields array in String format (comma delimated)
  303.      *      
  304.      * @return string Values of $_fields array in String format (comma delimated)
  305.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  306.      */
  307.     public function getStrFields()
  308.     {
  309.         $fields implode(','$this->_fields);
  310.         
  311.         return $fields;
  312.     }
  313.     
  314.     /**
  315.      * Build WHERE section of the SQL statement using defind lex's rules, search
  316.      * mode [AND | OR], and handle also phrases (inclosed by "") using normal
  317.      * LIKE condition to match it as it is.
  318.      *      
  319.      * @param string $arg String that user search for in the database table
  320.      *                    
  321.      * @return string The WHERE section in SQL statement
  322.      *                 (MySQL database engine format)
  323.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  324.      */
  325.     public function getWhereCondition($arg)
  326.     {
  327.         $sql '';
  328.         $arg mysql_escape_string($arg);
  329.         
  330.         // Check if there are phrases in $arg should handle as it is
  331.         $phrase explode("\""$arg);
  332.         
  333.         if (count($phrase2{
  334.             // Re-init $arg variable
  335.             // (It will contain the rest of $arg except phrases).
  336.             $arg '';
  337.             
  338.             for ($i 0$i count($phrase)$i++{
  339.                 $subPhrase $phrase[$i]
  340.                 if ($i == && $subPhrase != ''{
  341.                     // Re-build $arg variable after restricting phrases
  342.                     $arg .= $subPhrase;
  343.                 elseif ($i == && $subPhrase != ''{
  344.                     // Handle phrases using reqular LIKE matching in MySQL
  345.                     $this->wordCondition[$this->getWordLike($subPhrase);
  346.                 }
  347.             }
  348.         }
  349.         
  350.         // Handle normal $arg using lex's and regular expresion
  351.         $words preg_split('/\s+/'trim($arg));
  352.         
  353.         foreach ($words as $word{
  354.             //if (is_numeric($word) || strlen($word) > 2) {
  355.                 // Take off all the punctuation
  356.                 //$word = preg_replace("/\p{P}/", '', $word);
  357.                 $exclude array('('')''['']''{''}'','';'':'
  358.                                  '?''!''،''؛''؟');
  359.                 $word    str_replace($exclude''$word);
  360.  
  361.                 $this->wordCondition[$this->getWordRegExp($word);
  362.             //}
  363.         }
  364.         
  365.         if (!empty($this->wordCondition)) {
  366.             if ($this->mode == 0{
  367.                 $sql '(' implode(') OR ('$this->wordCondition')';
  368.             elseif ($this->mode == 1{
  369.                 $sql '(' implode(') AND ('$this->wordCondition')';
  370.             }
  371.         }
  372.         
  373.         return $sql;
  374.     }
  375.     
  376.     /**
  377.      * Search condition in SQL format for one word in all defind fields using
  378.      * REGEXP clause and lex's rules
  379.      *      
  380.      * @param string $arg String (one word) that you want to build a condition for
  381.      *      
  382.      * @return string sub SQL condition (for internal use)
  383.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  384.      */
  385.     protected function getWordRegExp($arg)
  386.     {
  387.         $arg $this->lex($arg);
  388.         //$sql = implode(" REGEXP '$arg' OR ", $this->_fields) . " REGEXP '$arg'";
  389.         $sql ' REPLACE(' 
  390.                implode(", 'ـ', '') REGEXP '$arg' OR REPLACE("$this->_fields
  391.                ", 'ـ', '') REGEXP '$arg'";
  392.  
  393.         
  394.         return $sql;
  395.     }
  396.     
  397.     /**
  398.      * Search condition in SQL format for one word in all defind fields using
  399.      * normal LIKE clause
  400.      *      
  401.      * @param string $arg String (one word) that you want to build a condition for
  402.      *      
  403.      * @return string sub SQL condition (for internal use)
  404.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  405.      */
  406.     protected function getWordLike($arg)
  407.     {
  408.         $sql implode(" LIKE '$arg' OR "$this->_fields" LIKE '$arg'";
  409.         
  410.         return $sql;
  411.     }
  412.     
  413.     /**
  414.      * Get more relevant order by section related to the user search keywords
  415.      *      
  416.      * @param string $arg String that user search for in the database table
  417.      *                    
  418.      * @return string sub SQL ORDER BY section
  419.      * @author Saleh AlMatrafe <saleh@saleh.cc>
  420.      */
  421.     public function getOrderBy($arg)
  422.     {
  423.         // Check if there are phrases in $arg should handle as it is
  424.         $phrase explode("\""$arg);
  425.         if (count($phrase2{
  426.             // Re-init $arg variable 
  427.             // (It will contain the rest of $arg except phrases).
  428.             $arg '';
  429.             for ($i 0$i count($phrase)$i++{
  430.                 if ($i == && $phrase[$i!= ''{
  431.                     // Re-build $arg variable after restricting phrases
  432.                     $arg .= $phrase[$i];
  433.                 elseif ($i == && $phrase[$i!= ''{
  434.                     // Handle phrases using reqular LIKE matching in MySQL
  435.                     $wordOrder[$this->getWordLike($phrase[$i]);
  436.                 }
  437.             }
  438.         }
  439.         
  440.         // Handle normal $arg using lex's and regular expresion
  441.         $words explode(' '$arg);
  442.         foreach ($words as $word{
  443.             if ($word != ''{
  444.                 $wordOrder['CASE WHEN ' 
  445.                                $this->getWordRegExp($word
  446.                                ' THEN 1 ELSE 0 END';
  447.             }
  448.         }
  449.         
  450.         $order '((' implode(') + ('$wordOrder')) DESC';
  451.         
  452.         return $order;
  453.     }
  454.  
  455.     /**
  456.      * This method will implement various regular expressin rules based on
  457.      * pre-defined Arabic lexical rules
  458.      *      
  459.      * @param string $arg String of one word user want to search for
  460.      *      
  461.      * @return string Regular Expression format to be used in MySQL query statement
  462.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  463.      */
  464.     protected function lex($arg)
  465.     {
  466.         $arg preg_replace($this->_lexPatterns$this->_lexReplacements$arg);
  467.         
  468.         return $arg;
  469.     }
  470.     
  471.     /**
  472.      * Get most possible Arabic lexical forms for a given word
  473.      *      
  474.      * @param string $word String that user search for
  475.      *      
  476.      * @return string list of most possible Arabic lexical forms for a given word
  477.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  478.      */
  479.     protected function allWordForms($word
  480.     {
  481.         $wordForms array($word);
  482.         
  483.         $postfix1 array('كم''كن''نا''ها''هم''هن');
  484.         $postfix2 array('ين''ون''ان''ات''وا');
  485.         
  486.         $len mb_strlen($word);
  487.  
  488.         if (mb_substr($word02== 'ال'{
  489.             $word mb_substr($word2);
  490.         }
  491.         
  492.         $wordForms[$word;
  493.  
  494.         $str1 mb_substr($word0-1);
  495.         $str2 mb_substr($word0-2);
  496.         $str3 mb_substr($word0-3);
  497.  
  498.         $last1 mb_substr($word-1);
  499.         $last2 mb_substr($word-2);
  500.         $last3 mb_substr($word-3);
  501.         
  502.         if ($len >= && $last3 == 'تين'{
  503.             $wordForms[$str3;
  504.             $wordForms[$str3 'ة';
  505.             $wordForms[$word 'ة';
  506.         }
  507.         
  508.         if ($len >= && ($last3 == 'كما' || $last3 == 'هما')) {
  509.             $wordForms[$str3;
  510.             $wordForms[$str3 'كما';
  511.             $wordForms[$str3 'هما';
  512.         }
  513.  
  514.         if ($len >= && in_array($last2$postfix2)) {
  515.             $wordForms[$str2;
  516.             $wordForms[$str2.'ة';
  517.             $wordForms[$str2.'تين';
  518.  
  519.             foreach ($postfix2 as $postfix{
  520.                 $wordForms[$str2 $postfix;
  521.             }
  522.         }
  523.  
  524.         if ($len >= && in_array($last2$postfix1)) {
  525.             $wordForms[$str2;
  526.             $wordForms[$str2.'ي';
  527.             $wordForms[$str2.'ك';
  528.             $wordForms[$str2.'كما';
  529.             $wordForms[$str2.'هما';
  530.  
  531.             foreach ($postfix1 as $postfix{
  532.                 $wordForms[$str2 $postfix;
  533.             }
  534.         }
  535.  
  536.         if ($len >= && $last2 == 'ية'{
  537.             $wordForms[$str1;
  538.             $wordForms[$str2;
  539.         }
  540.  
  541.         if (($len >= && ($last1 == 'ة' || $last1 == 'ه' || $last1 == 'ت')) 
  542.             || ($len >= && $last2 == 'ات')
  543.         {
  544.             $wordForms[$str1;
  545.             $wordForms[$str1 'ة';
  546.             $wordForms[$str1 'ه';
  547.             $wordForms[$str1 'ت';
  548.             $wordForms[$str1 'ات';
  549.         }
  550.         
  551.         if ($len >= && $last1 == 'ى'{
  552.             $wordForms[$str1 'ا';
  553.         }
  554.  
  555.         $trans array('أ' => 'ا''إ' => 'ا''آ' => 'ا');
  556.         foreach ($wordForms as $word{
  557.             $normWord strtr($word$trans);
  558.             if ($normWord != $word{
  559.                 $wordForms[$normWord;
  560.             }
  561.         }
  562.         
  563.         $wordForms array_unique($wordForms);
  564.         
  565.         return $wordForms;
  566.     }
  567.     
  568.     /**
  569.      * Get most possible Arabic lexical forms of user search keywords
  570.      *      
  571.      * @param string $arg String that user search for
  572.      *                    
  573.      * @return string list of most possible Arabic lexical forms for given keywords
  574.      * @author Khaled Al-Sham'aa <khaled@ar-php.org>
  575.      */
  576.     public function allForms($arg)
  577.     {
  578.         $wordForms array();
  579.         $words     explode(' '$arg);
  580.         
  581.         foreach ($words as $word{
  582.             $wordForms array_merge($wordForms$this->allWordForms($word));
  583.         }
  584.         
  585.         $str implode(' '$wordForms);
  586.         
  587.         return $str;
  588.     }
  589. }

Documentation generated on Mon, 14 Jan 2013 17:49:03 +0100 by phpDocumentor 1.4.0