Initial commit
[moodle.git] / search / Zend / Search / Lucene / Search / Query / MultiTerm.php
1 <?php
2 /**
3  * Zend Framework
4  *
5  * LICENSE
6  *
7  * This source file is subject to the new BSD license that is bundled
8  * with this package in the file LICENSE.txt.
9  * It is also available through the world-wide-web at this URL:
10  * http://framework.zend.com/license/new-bsd
11  * If you did not receive a copy of the license and are unable to
12  * obtain it through the world-wide-web, please send an email
13  * to license@zend.com so we can send you a copy immediately.
14  *
15  * @category   Zend
16  * @package    Zend_Search_Lucene
17  * @subpackage Search
18  * @copyright  Copyright (c) 2006 Zend Technologies USA Inc. (http://www.zend.com)
19  * @license    http://framework.zend.com/license/new-bsd     New BSD License
20  */
23 /** Zend_Search_Lucene_Search_Query */
24 require_once 'Zend/Search/Lucene/Search/Query.php';
26 /** Zend_Search_Lucene_Search_Weight_MultiTerm */
27 require_once 'Zend/Search/Lucene/Search/Weight/MultiTerm.php';
30 /**
31  * @category   Zend
32  * @package    Zend_Search_Lucene
33  * @subpackage Search
34  * @copyright  Copyright (c) 2006 Zend Technologies USA Inc. (http://www.zend.com)
35  * @license    http://framework.zend.com/license/new-bsd     New BSD License
36  */
37 class Zend_Search_Lucene_Search_Query_MultiTerm extends Zend_Search_Lucene_Search_Query
38 {
40     /**
41      * Terms to find.
42      * Array of Zend_Search_Lucene_Index_Term
43      *
44      * @var array
45      */
46     private $_terms = array();
48     /**
49      * Term signs.
50      * If true then term is required.
51      * If false then term is prohibited.
52      * If null then term is neither prohibited, nor required
53      *
54      * If array is null then all terms are required
55      *
56      * @var array
57      */
59     private $_signs = array();
61     /**
62      * Result vector.
63      * Bitset or array of document IDs
64      * (depending from Bitset extension availability).
65      *
66      * @var mixed
67      */
68     private $_resVector = null;
70     /**
71      * Terms positions vectors.
72      * Array of Arrays:
73      * term1Id => (docId => array( pos1, pos2, ... ), ...)
74      * term2Id => (docId => array( pos1, pos2, ... ), ...)
75      *
76      * @var array
77      */
78     private $_termsPositions = array();
81     /**
82      * A score factor based on the fraction of all query terms
83      * that a document contains.
84      * float for conjunction queries
85      * array of float for non conjunction queries
86      *
87      * @var mixed
88      */
89     private $_coord = null;
92     /**
93      * Terms weights
94      * array of Zend_Search_Lucene_Search_Weight
95      *
96      * @var array
97      */
98     private $_weights = array();
101     /**
102      * Class constructor.  Create a new multi-term query object.
103      *
104      * @param array $terms    Array of Zend_Search_Lucene_Index_Term objects
105      * @param array $signs    Array of signs.  Sign is boolean|null.
106      * @return void
107      */
108     public function __construct($terms = null, $signs = null)
109     {
110         /**
111          * @todo Check contents of $terms and $signs before adding them.
112          */
113         if (is_array($terms)) {
114             $this->_terms = $terms;
116             $this->_signs = null;
117             // Check if all terms are required
118             if (is_array($signs)) {
119                 foreach ($signs as $sign ) {
120                     if ($sign !== true) {
121                         $this->_signs = $signs;
122                         continue;
123                     }
124                 }
125             }
126         }
127     }
130     /**
131      * Add a $term (Zend_Search_Lucene_Index_Term) to this query.
132      *
133      * The sign is specified as:
134      *     TRUE  - term is required
135      *     FALSE - term is prohibited
136      *     NULL  - term is neither prohibited, nor required
137      *
138      * @param  Zend_Search_Lucene_Index_Term $term
139      * @param  boolean|null $sign
140      * @return void
141      */
142     public function addTerm(Zend_Search_Lucene_Index_Term $term, $sign=null) {
143         $this->_terms[] = $term;
145         /**
146          * @todo This is not good.  Sometimes $this->_signs is an array, sometimes
147          * it is null, even when there are terms.  It will be changed so that
148          * it is always an array.
149          */
150         if ($this->_signs === null) {
151             if ($sign !== null) {
152                 $this->_signs = array();
153                 foreach ($this->_terms as $term) {
154                     $this->_signs[] = null;
155                 }
156                 $this->_signs[] = $sign;
157             }
158         } else {
159             $this->_signs[] = $sign;
160         }
161     }
164     /**
165      * Returns query term
166      *
167      * @return array
168      */
169     public function getTerms()
170     {
171         return $this->_terms;
172     }
175     /**
176      * Return terms signs
177      *
178      * @return array
179      */
180     public function getSigns()
181     {
182         return $this->_signs;
183     }
186     /**
187      * Set weight for specified term
188      *
189      * @param integer $num
190      * @param Zend_Search_Lucene_Search_Weight_Term $weight
191      */
192     public function setWeight($num, $weight)
193     {
194         $this->_weights[$num] = $weight;
195     }
198     /**
199      * Constructs an appropriate Weight implementation for this query.
200      *
201      * @param Zend_Search_Lucene $reader
202      * @return Zend_Search_Lucene_Search_Weight
203      */
204     protected function _createWeight($reader)
205     {
206         return new Zend_Search_Lucene_Search_Weight_MultiTerm($this, $reader);
207     }
210     /**
211      * Calculate result vector for Conjunction query
212      * (like '+something +another')
213      *
214      * @param Zend_Search_Lucene $reader
215      */
216     private function _calculateConjunctionResult($reader)
217     {
218         if (extension_loaded('bitset')) {
219             foreach( $this->_terms as $termId=>$term ) {
220                 if($this->_resVector === null) {
221                     $this->_resVector = bitset_from_array($reader->termDocs($term));
222                 } else {
223                     $this->_resVector = bitset_intersection(
224                                 $this->_resVector,
225                                 bitset_from_array($reader->termDocs($term)) );
226                 }
228                 $this->_termsPositions[$termId] = $reader->termPositions($term);
229             }
230         } else {
231             foreach( $this->_terms as $termId=>$term ) {
232                 if($this->_resVector === null) {
233                     $this->_resVector = array_flip($reader->termDocs($term));
234                 } else {
235                     $termDocs = array_flip($reader->termDocs($term));
236                     foreach($this->_resVector as $key=>$value) {
237                         if (!isset( $termDocs[$key] )) {
238                             unset( $this->_resVector[$key] );
239                         }
240                     }
241                 }
243                 $this->_termsPositions[$termId] = $reader->termPositions($term);
244             }
245         }
246     }
249     /**
250      * Calculate result vector for non Conjunction query
251      * (like '+something -another')
252      *
253      * @param Zend_Search_Lucene $reader
254      */
255     private function _calculateNonConjunctionResult($reader)
256     {
257         if (extension_loaded('bitset')) {
258             $required   = null;
259             $neither    = bitset_empty();
260             $prohibited = bitset_empty();
262             foreach ($this->_terms as $termId => $term) {
263                 $termDocs = bitset_from_array($reader->termDocs($term));
265                 if ($this->_signs[$termId] === true) {
266                     // required
267                     if ($required !== null) {
268                         $required = bitset_intersection($required, $termDocs);
269                     } else {
270                         $required = $termDocs;
271                     }
272                 } elseif ($this->_signs[$termId] === false) {
273                     // prohibited
274                     $prohibited = bitset_union($prohibited, $termDocs);
275                 } else {
276                     // neither required, nor prohibited
277                     $neither = bitset_union($neither, $termDocs);
278                 }
280                 $this->_termsPositions[$termId] = $reader->termPositions($term);
281             }
283             if ($required === null) {
284                 $required = $neither;
285             }
286             $this->_resVector = bitset_intersection( $required,
287                                                      bitset_invert($prohibited, $reader->count()) );
288         } else {
289             $required   = null;
290             $neither    = array();
291             $prohibited = array();
293             foreach ($this->_terms as $termId => $term) {
294                 $termDocs = array_flip($reader->termDocs($term));
296                 if ($this->_signs[$termId] === true) {
297                     // required
298                     if ($required !== null) {
299                         // substitute for bitset_intersection
300                         foreach ($required as $key => $value) {
301                             if (!isset( $termDocs[$key] )) {
302                                 unset($required[$key]);
303                             }
304                         }
305                     } else {
306                         $required = $termDocs;
307                     }
308                 } elseif ($this->_signs[$termId] === false) {
309                     // prohibited
310                     // substitute for bitset_union
311                     foreach ($termDocs as $key => $value) {
312                         $prohibited[$key] = $value;
313                     }
314                 } else {
315                     // neither required, nor prohibited
316                     // substitute for bitset_union
317                     foreach ($termDocs as $key => $value) {
318                         $neither[$key] = $value;
319                     }
320                 }
322                 $this->_termsPositions[$termId] = $reader->termPositions($term);
323             }
325             if ($required === null) {
326                 $required = $neither;
327             }
329             foreach ($required as $key=>$value) {
330                 if (isset( $prohibited[$key] )) {
331                     unset($required[$key]);
332                 }
333             }
334             $this->_resVector = $required;
335         }
336     }
339     /**
340      * Score calculator for conjunction queries (all terms are required)
341      *
342      * @param integer $docId
343      * @param Zend_Search_Lucene $reader
344      * @return float
345      */
346     public function _conjunctionScore($docId, $reader)
347     {
348         if ($this->_coord === null) {
349             $this->_coord = $reader->getSimilarity()->coord(count($this->_terms),
350                                                             count($this->_terms) );
351         }
353         $score = 0.0;
355         foreach ($this->_terms as $termId=>$term) {
356             $score += $reader->getSimilarity()->tf(count($this->_termsPositions[$termId][$docId]) ) *
357                       $this->_weights[$termId]->getValue() *
358                       $reader->norm($docId, $term->field);
359         }
361         return $score * $this->_coord;
362     }
365     /**
366      * Score calculator for non conjunction queries (not all terms are required)
367      *
368      * @param integer $docId
369      * @param Zend_Search_Lucene $reader
370      * @return float
371      */
372     public function _nonConjunctionScore($docId, $reader)
373     {
374         if ($this->_coord === null) {
375             $this->_coord = array();
377             $maxCoord = 0;
378             foreach ($this->_signs as $sign) {
379                 if ($sign !== false /* not prohibited */) {
380                     $maxCoord++;
381                 }
382             }
384             for ($count = 0; $count <= $maxCoord; $count++) {
385                 $this->_coord[$count] = $reader->getSimilarity()->coord($count, $maxCoord);
386             }
387         }
389         $score = 0.0;
390         $matchedTerms = 0;
391         foreach ($this->_terms as $termId=>$term) {
392             // Check if term is
393             if ($this->_signs[$termId] !== false &&            // not prohibited
394                 isset($this->_termsPositions[$termId][$docId]) // matched
395                ) {
396                 $matchedTerms++;
397                 $score +=
398                       $reader->getSimilarity()->tf(count($this->_termsPositions[$termId][$docId]) ) *
399                       $this->_weights[$termId]->getValue() *
400                       $reader->norm($docId, $term->field);
401             }
402         }
404         return $score * $this->_coord[$matchedTerms];
405     }
407     /**
408      * Score specified document
409      *
410      * @param integer $docId
411      * @param Zend_Search_Lucene $reader
412      * @return float
413      */
414     public function score($docId, $reader)
415     {
416         if($this->_resVector === null) {
417             if ($this->_signs === null) {
418                 $this->_calculateConjunctionResult($reader);
419             } else {
420                 $this->_calculateNonConjunctionResult($reader);
421             }
423             $this->_initWeight($reader);
424         }
426         if ( (extension_loaded('bitset')) ?
427                 bitset_in($this->_resVector, $docId) :
428                 isset($this->_resVector[$docId])  ) {
429             if ($this->_signs === null) {
430                 return $this->_conjunctionScore($docId, $reader);
431             } else {
432                 return $this->_nonConjunctionScore($docId, $reader);
433             }
434         } else {
435             return 0;
436         }
437     }