import
[web.mtrack] / inc / lib / Zend / Search / Lucene / Search / QueryParserContext.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) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
19  * @license    http://framework.zend.com/license/new-bsd     New BSD License
20  * @version    $Id: QueryParserContext.php 16971 2009-07-22 18:05:45Z mikaelkael $
21  */
22
23 /** Zend_Search_Lucene_FSM */
24 require_once 'Zend/Search/Lucene/FSM.php';
25
26 /** Zend_Search_Lucene_Index_Term */
27 require_once 'Zend/Search/Lucene/Index/Term.php';
28
29 /** Zend_Search_Lucene_Search_QueryToken */
30 require_once 'Zend/Search/Lucene/Search/QueryToken.php';
31
32 /** Zend_Search_Lucene_Search_Query_Term */
33 require_once 'Zend/Search/Lucene/Search/Query/Term.php';
34
35 /** Zend_Search_Lucene_Search_Query_MultiTerm */
36 require_once 'Zend/Search/Lucene/Search/Query/MultiTerm.php';
37
38 /** Zend_Search_Lucene_Search_Query_Boolean */
39 require_once 'Zend/Search/Lucene/Search/Query/Boolean.php';
40
41 /** Zend_Search_Lucene_Search_Query_Phrase */
42 require_once 'Zend/Search/Lucene/Search/Query/Phrase.php';
43
44 /** Zend_Search_Lucene_Search_BooleanExpressionRecognizer */
45 require_once 'Zend/Search/Lucene/Search/BooleanExpressionRecognizer.php';
46
47 /** Zend_Search_Lucene_Search_QueryEntry */
48 require_once 'Zend/Search/Lucene/Search/QueryEntry.php';
49
50 /**
51  * @category   Zend
52  * @package    Zend_Search_Lucene
53  * @subpackage Search
54  * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
55  * @license    http://framework.zend.com/license/new-bsd     New BSD License
56  */
57 class Zend_Search_Lucene_Search_QueryParserContext
58 {
59     /**
60      * Default field for the context.
61      *
62      * null means, that term should be searched through all fields
63      * Zend_Search_Lucene_Search_Query::rewriteQuery($index) transletes such queries to several
64      *
65      * @var string|null
66      */
67     private $_defaultField;
68
69     /**
70      * Field specified for next entry
71      *
72      * @var string
73      */
74     private $_nextEntryField = null;
75
76     /**
77      * True means, that term is required.
78      * False means, that term is prohibited.
79      * null means, that term is neither prohibited, nor required
80      *
81      * @var boolean
82      */
83     private $_nextEntrySign = null;
84
85
86     /**
87      * Entries grouping mode
88      */
89     const GM_SIGNS   = 0;  // Signs mode: '+term1 term2 -term3 +(subquery1) -(subquery2)'
90     const GM_BOOLEAN = 1;  // Boolean operators mode: 'term1 and term2  or  (subquery1) and not (subquery2)'
91
92     /**
93      * Grouping mode
94      *
95      * @var integer
96      */
97     private $_mode = null;
98
99     /**
100      * Entries signs.
101      * Used in GM_SIGNS grouping mode
102      *
103      * @var arrays
104      */
105     private $_signs = array();
106
107     /**
108      * Query entries
109      * Each entry is a Zend_Search_Lucene_Search_QueryEntry object or
110      * boolean operator (Zend_Search_Lucene_Search_QueryToken class constant)
111      *
112      * @var array
113      */
114     private $_entries = array();
115
116     /**
117      * Query string encoding
118      *
119      * @var string
120      */
121     private $_encoding;
122
123
124     /**
125      * Context object constructor
126      *
127      * @param string $encoding
128      * @param string|null $defaultField
129      */
130     public function __construct($encoding, $defaultField = null)
131     {
132         $this->_encoding     = $encoding;
133         $this->_defaultField = $defaultField;
134     }
135
136
137     /**
138      * Get context default field
139      *
140      * @return string|null
141      */
142     public function getField()
143     {
144         return ($this->_nextEntryField !== null)  ?  $this->_nextEntryField : $this->_defaultField;
145     }
146
147     /**
148      * Set field for next entry
149      *
150      * @param string $field
151      */
152     public function setNextEntryField($field)
153     {
154         $this->_nextEntryField = $field;
155     }
156
157
158     /**
159      * Set sign for next entry
160      *
161      * @param integer $sign
162      * @throws Zend_Search_Lucene_Exception
163      */
164     public function setNextEntrySign($sign)
165     {
166         if ($this->_mode === self::GM_BOOLEAN) {
167             require_once 'Zend/Search/Lucene/Search/QueryParserException.php';
168             throw new Zend_Search_Lucene_Search_QueryParserException('It\'s not allowed to mix boolean and signs styles in the same subquery.');
169         }
170
171         $this->_mode = self::GM_SIGNS;
172
173         if ($sign == Zend_Search_Lucene_Search_QueryToken::TT_REQUIRED) {
174             $this->_nextEntrySign = true;
175         } else if ($sign == Zend_Search_Lucene_Search_QueryToken::TT_PROHIBITED) {
176             $this->_nextEntrySign = false;
177         } else {
178             require_once 'Zend/Search/Lucene/Exception.php';
179             throw new Zend_Search_Lucene_Exception('Unrecognized sign type.');
180         }
181     }
182
183
184     /**
185      * Add entry to a query
186      *
187      * @param Zend_Search_Lucene_Search_QueryEntry $entry
188      */
189     public function addEntry(Zend_Search_Lucene_Search_QueryEntry $entry)
190     {
191         if ($this->_mode !== self::GM_BOOLEAN) {
192             $this->_signs[] = $this->_nextEntrySign;
193         }
194
195         $this->_entries[] = $entry;
196
197         $this->_nextEntryField = null;
198         $this->_nextEntrySign  = null;
199     }
200
201
202     /**
203      * Process fuzzy search or proximity search modifier
204      *
205      * @throws Zend_Search_Lucene_Search_QueryParserException
206      */
207     public function processFuzzyProximityModifier($parameter = null)
208     {
209         // Check, that modifier has came just after word or phrase
210         if ($this->_nextEntryField !== null  ||  $this->_nextEntrySign !== null) {
211             require_once 'Zend/Search/Lucene/Search/QueryParserException.php';
212             throw new Zend_Search_Lucene_Search_QueryParserException('\'~\' modifier must follow word or phrase.');
213         }
214
215         $lastEntry = array_pop($this->_entries);
216
217         if (!$lastEntry instanceof Zend_Search_Lucene_Search_QueryEntry) {
218             // there are no entries or last entry is boolean operator
219             require_once 'Zend/Search/Lucene/Search/QueryParserException.php';
220             throw new Zend_Search_Lucene_Search_QueryParserException('\'~\' modifier must follow word or phrase.');
221         }
222
223         $lastEntry->processFuzzyProximityModifier($parameter);
224
225         $this->_entries[] = $lastEntry;
226     }
227
228     /**
229      * Set boost factor to the entry
230      *
231      * @param float $boostFactor
232      */
233     public function boost($boostFactor)
234     {
235         // Check, that modifier has came just after word or phrase
236         if ($this->_nextEntryField !== null  ||  $this->_nextEntrySign !== null) {
237             require_once 'Zend/Search/Lucene/Search/QueryParserException.php';
238             throw new Zend_Search_Lucene_Search_QueryParserException('\'^\' modifier must follow word, phrase or subquery.');
239         }
240
241         $lastEntry = array_pop($this->_entries);
242
243         if (!$lastEntry instanceof Zend_Search_Lucene_Search_QueryEntry) {
244             // there are no entries or last entry is boolean operator
245             require_once 'Zend/Search/Lucene/Search/QueryParserException.php';
246             throw new Zend_Search_Lucene_Search_QueryParserException('\'^\' modifier must follow word, phrase or subquery.');
247         }
248
249         $lastEntry->boost($boostFactor);
250
251         $this->_entries[] = $lastEntry;
252     }
253
254     /**
255      * Process logical operator
256      *
257      * @param integer $operator
258      */
259     public function addLogicalOperator($operator)
260     {
261         if ($this->_mode === self::GM_SIGNS) {
262             require_once 'Zend/Search/Lucene/Search/QueryParserException.php';
263             throw new Zend_Search_Lucene_Search_QueryParserException('It\'s not allowed to mix boolean and signs styles in the same subquery.');
264         }
265
266         $this->_mode = self::GM_BOOLEAN;
267
268         $this->_entries[] = $operator;
269     }
270
271
272     /**
273      * Generate 'signs style' query from the context
274      * '+term1 term2 -term3 +(<subquery1>) ...'
275      *
276      * @return Zend_Search_Lucene_Search_Query
277      */
278     public function _signStyleExpressionQuery()
279     {
280         $query = new Zend_Search_Lucene_Search_Query_Boolean();
281
282         if (Zend_Search_Lucene_Search_QueryParser::getDefaultOperator() == Zend_Search_Lucene_Search_QueryParser::B_AND) {
283             $defaultSign = true; // required
284         } else {
285             // Zend_Search_Lucene_Search_QueryParser::B_OR
286             $defaultSign = null; // optional
287         }
288
289         foreach ($this->_entries as $entryId => $entry) {
290             $sign = ($this->_signs[$entryId] !== null) ?  $this->_signs[$entryId] : $defaultSign;
291             $query->addSubquery($entry->getQuery($this->_encoding), $sign);
292         }
293
294         return $query;
295     }
296
297
298     /**
299      * Generate 'boolean style' query from the context
300      * 'term1 and term2   or   term3 and (<subquery1>) and not (<subquery2>)'
301      *
302      * @return Zend_Search_Lucene_Search_Query
303      * @throws Zend_Search_Lucene
304      */
305     private function _booleanExpressionQuery()
306     {
307         /**
308          * We treat each level of an expression as a boolean expression in
309          * a Disjunctive Normal Form
310          *
311          * AND operator has higher precedence than OR
312          *
313          * Thus logical query is a disjunction of one or more conjunctions of
314          * one or more query entries
315          */
316
317         $expressionRecognizer = new Zend_Search_Lucene_Search_BooleanExpressionRecognizer();
318
319         require_once 'Zend/Search/Lucene/Exception.php';
320         try {
321             foreach ($this->_entries as $entry) {
322                 if ($entry instanceof Zend_Search_Lucene_Search_QueryEntry) {
323                     $expressionRecognizer->processLiteral($entry);
324                 } else {
325                     switch ($entry) {
326                         case Zend_Search_Lucene_Search_QueryToken::TT_AND_LEXEME:
327                             $expressionRecognizer->processOperator(Zend_Search_Lucene_Search_BooleanExpressionRecognizer::IN_AND_OPERATOR);
328                             break;
329
330                         case Zend_Search_Lucene_Search_QueryToken::TT_OR_LEXEME:
331                             $expressionRecognizer->processOperator(Zend_Search_Lucene_Search_BooleanExpressionRecognizer::IN_OR_OPERATOR);
332                             break;
333
334                         case Zend_Search_Lucene_Search_QueryToken::TT_NOT_LEXEME:
335                             $expressionRecognizer->processOperator(Zend_Search_Lucene_Search_BooleanExpressionRecognizer::IN_NOT_OPERATOR);
336                             break;
337
338                         default:
339                             throw new Zend_Search_Lucene('Boolean expression error. Unknown operator type.');
340                     }
341                 }
342             }
343
344             $conjuctions = $expressionRecognizer->finishExpression();
345         } catch (Zend_Search_Exception $e) {
346             // throw new Zend_Search_Lucene_Search_QueryParserException('Boolean expression error. Error message: \'' .
347             //                                                          $e->getMessage() . '\'.' );
348             // It's query syntax error message and it should be user friendly. So FSM message is omitted
349             require_once 'Zend/Search/Lucene/Search/QueryParserException.php';
350             throw new Zend_Search_Lucene_Search_QueryParserException('Boolean expression error.');
351         }
352
353         // Remove 'only negative' conjunctions
354         foreach ($conjuctions as $conjuctionId => $conjuction) {
355             $nonNegativeEntryFound = false;
356
357             foreach ($conjuction as $conjuctionEntry) {
358                 if ($conjuctionEntry[1]) {
359                     $nonNegativeEntryFound = true;
360                     break;
361                 }
362             }
363
364             if (!$nonNegativeEntryFound) {
365                 unset($conjuctions[$conjuctionId]);
366             }
367         }
368
369
370         $subqueries = array();
371         foreach ($conjuctions as  $conjuction) {
372             // Check, if it's a one term conjuction
373             if (count($conjuction) == 1) {
374                 $subqueries[] = $conjuction[0][0]->getQuery($this->_encoding);
375             } else {
376                 $subquery = new Zend_Search_Lucene_Search_Query_Boolean();
377
378                 foreach ($conjuction as $conjuctionEntry) {
379                     $subquery->addSubquery($conjuctionEntry[0]->getQuery($this->_encoding), $conjuctionEntry[1]);
380                 }
381
382                 $subqueries[] = $subquery;
383             }
384         }
385
386         if (count($subqueries) == 0) {
387             return new Zend_Search_Lucene_Search_Query_Insignificant();
388         }
389
390         if (count($subqueries) == 1) {
391             return $subqueries[0];
392         }
393
394
395         $query = new Zend_Search_Lucene_Search_Query_Boolean();
396
397         foreach ($subqueries as $subquery) {
398             // Non-requirered entry/subquery
399             $query->addSubquery($subquery);
400         }
401
402         return $query;
403     }
404
405     /**
406      * Generate query from current context
407      *
408      * @return Zend_Search_Lucene_Search_Query
409      */
410     public function getQuery()
411     {
412         if ($this->_mode === self::GM_BOOLEAN) {
413             return $this->_booleanExpressionQuery();
414         } else {
415             return $this->_signStyleExpressionQuery();
416         }
417     }
418 }