php8
[web.mtrack] / Zend / Search / Lucene.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  * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
18  * @license    http://framework.zend.com/license/new-bsd     New BSD License
19  * @version    $Id: Lucene.php 17164 2009-07-27 03:59:23Z matthew $
20  */
21
22 /** Zend_Search_Lucene_Document */
23 require_once 'Zend/Search/Lucene/Document.php';
24
25 /** Zend_Search_Lucene_Document_Html */
26 require_once 'Zend/Search/Lucene/Document/Html.php';
27
28 /** Zend_Search_Lucene_Document_Docx */
29 require_once 'Zend/Search/Lucene/Document/Docx.php';
30
31 /** Zend_Search_Lucene_Document_Pptx */
32 require_once 'Zend/Search/Lucene/Document/Pptx.php';
33
34 /** Zend_Search_Lucene_Document_Xlsx */
35 require_once 'Zend/Search/Lucene/Document/Xlsx.php';
36
37 /** Zend_Search_Lucene_Storage_Directory_Filesystem */
38 require_once 'Zend/Search/Lucene/Storage/Directory/Filesystem.php';
39
40 /** Zend_Search_Lucene_Storage_File_Memory */
41 require_once 'Zend/Search/Lucene/Storage/File/Memory.php';
42
43 /** Zend_Search_Lucene_Index_Term */
44 require_once 'Zend/Search/Lucene/Index/Term.php';
45
46 /** Zend_Search_Lucene_Index_TermInfo */
47 require_once 'Zend/Search/Lucene/Index/TermInfo.php';
48
49 /** Zend_Search_Lucene_Index_SegmentInfo */
50 require_once 'Zend/Search/Lucene/Index/SegmentInfo.php';
51
52 /** Zend_Search_Lucene_Index_FieldInfo */
53 require_once 'Zend/Search/Lucene/Index/FieldInfo.php';
54
55 /** Zend_Search_Lucene_Index_Writer */
56 require_once 'Zend/Search/Lucene/Index/Writer.php';
57
58 /** Zend_Search_Lucene_Search_QueryParser */
59 require_once 'Zend/Search/Lucene/Search/QueryParser.php';
60
61 /** Zend_Search_Lucene_Search_QueryHit */
62 require_once 'Zend/Search/Lucene/Search/QueryHit.php';
63
64 /** Zend_Search_Lucene_Search_Similarity */
65 require_once 'Zend/Search/Lucene/Search/Similarity.php';
66
67 /** Zend_Search_Lucene_Index_TermsPriorityQueue */
68 require_once 'Zend/Search/Lucene/Index/TermsPriorityQueue.php';
69
70 /** Zend_Search_Lucene_TermStreamsPriorityQueue */
71 require_once 'Zend/Search/Lucene/TermStreamsPriorityQueue.php';
72
73 /** Zend_Search_Lucene_Index_DocsFilter */
74 require_once 'Zend/Search/Lucene/Index/DocsFilter.php';
75
76 /** Zend_Search_Lucene_LockManager */
77 require_once 'Zend/Search/Lucene/LockManager.php';
78
79 /** Zend_Search_Lucene_Interface */
80 require_once 'Zend/Search/Lucene/Interface.php';
81
82 /** Zend_Search_Lucene_Proxy */
83 require_once 'Zend/Search/Lucene/Proxy.php';
84
85 /**
86  * @category   Zend
87  * @package    Zend_Search_Lucene
88  * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
89  * @license    http://framework.zend.com/license/new-bsd     New BSD License
90  */
91 class Zend_Search_Lucene implements Zend_Search_Lucene_Interface
92 {
93     /**
94      * Default field name for search
95      *
96      * Null means search through all fields
97      *
98      * @var string
99      */
100     private static $_defaultSearchField = null;
101
102     /**
103      * Result set limit
104      *
105      * 0 means no limit
106      *
107      * @var integer
108      */
109     private static $_resultSetLimit = 0;
110
111     /**
112      * Terms per query limit
113      *
114      * 0 means no limit
115      *
116      * @var integer
117      */
118     private static $_termsPerQueryLimit = 1024;
119
120     /**
121      * File system adapter.
122      *
123      * @var Zend_Search_Lucene_Storage_Directory
124      */
125     private $_directory = null;
126
127     /**
128      * File system adapter closing option
129      *
130      * @var boolean
131      */
132     private $_closeDirOnExit = true;
133
134     /**
135      * Writer for this index, not instantiated unless required.
136      *
137      * @var Zend_Search_Lucene_Index_Writer
138      */
139     private $_writer = null;
140
141     /**
142      * Array of Zend_Search_Lucene_Index_SegmentInfo objects for this index.
143      *
144      * @var array Zend_Search_Lucene_Index_SegmentInfo
145      */
146     private $_segmentInfos = array();
147
148     /**
149      * Number of documents in this index.
150      *
151      * @var integer
152      */
153     private $_docCount = 0;
154
155     /**
156      * Flag for index changes
157      *
158      * @var boolean
159      */
160     private $_hasChanges = false;
161
162
163     /**
164      * Signal, that index is already closed, changes are fixed and resources are cleaned up
165      *
166      * @var boolean
167      */
168     private $_closed = false;
169
170     /**
171      * Number of references to the index object
172      *
173      * @var integer
174      */
175     private $_refCount = 0;
176
177     /**
178      * Current segment generation
179      *
180      * @var integer
181      */
182     private $_generation;
183
184     const FORMAT_PRE_2_1 = 0;
185     const FORMAT_2_1     = 1;
186     const FORMAT_2_3     = 2;
187
188
189     /**
190      * Index format version
191      *
192      * @var integer
193      */
194     private $_formatVersion;
195
196     /**
197      * Create index
198      *
199      * @param mixed $directory
200      * @return Zend_Search_Lucene_Interface
201      */
202     public static function create($directory)
203     {
204         return new Zend_Search_Lucene_Proxy(new Zend_Search_Lucene($directory, true));
205     }
206
207     /**
208      * Open index
209      *
210      * @param mixed $directory
211      * @return Zend_Search_Lucene_Interface
212      */
213     public static function open($directory)
214     {
215         return new Zend_Search_Lucene_Proxy(new Zend_Search_Lucene($directory, false));
216     }
217
218     /** Generation retrieving counter */
219     const GENERATION_RETRIEVE_COUNT = 10;
220
221     /** Pause between generation retrieving attempts in milliseconds */
222     const GENERATION_RETRIEVE_PAUSE = 50;
223
224     /**
225      * Get current generation number
226      *
227      * Returns generation number
228      * 0 means pre-2.1 index format
229      * -1 means there are no segments files.
230      *
231      * @param Zend_Search_Lucene_Storage_Directory $directory
232      * @return integer
233      * @throws Zend_Search_Lucene_Exception
234      */
235     public static function getActualGeneration(Zend_Search_Lucene_Storage_Directory $directory)
236     {
237         /**
238          * Zend_Search_Lucene uses segments.gen file to retrieve current generation number
239          *
240          * Apache Lucene index format documentation mentions this method only as a fallback method
241          *
242          * Nevertheless we use it according to the performance considerations
243          *
244          * @todo check if we can use some modification of Apache Lucene generation determination algorithm
245          *       without performance problems
246          */
247
248         require_once 'Zend/Search/Lucene/Exception.php';
249         try {
250             for ($count = 0; $count < self::GENERATION_RETRIEVE_COUNT; $count++) {
251                 // Try to get generation file
252                 $genFile = $directory->getFileObject('segments.gen', false);
253
254                 $format = $genFile->readInt();
255                 if ($format != (int)0xFFFFFFFE) {
256                     throw new Zend_Search_Lucene_Exception('Wrong segments.gen file format');
257                 }
258
259                 $gen1 = $genFile->readLong();
260                 $gen2 = $genFile->readLong();
261
262                 if ($gen1 == $gen2) {
263                     return $gen1;
264                 }
265
266                 usleep(self::GENERATION_RETRIEVE_PAUSE * 1000);
267             }
268
269             // All passes are failed
270             throw new Zend_Search_Lucene_Exception('Index is under processing now');
271         } catch (Zend_Search_Lucene_Exception $e) {
272             if (strpos($e->getMessage(), 'is not readable') !== false) {
273                 try {
274                     // Try to open old style segments file
275                     $segmentsFile = $directory->getFileObject('segments', false);
276
277                     // It's pre-2.1 index
278                     return 0;
279                 } catch (Zend_Search_Lucene_Exception $e) {
280                     if (strpos($e->getMessage(), 'is not readable') !== false) {
281                         return -1;
282                     } else {
283                         throw $e;
284                     }
285                 }
286             } else {
287                 throw $e;
288             }
289         }
290
291         return -1;
292     }
293
294     /**
295      * Get segments file name
296      *
297      * @param integer $generation
298      * @return string
299      */
300     public static function getSegmentFileName($generation)
301     {
302         if ($generation == 0) {
303             return 'segments';
304         }
305
306         return 'segments_' . base_convert($generation, 10, 36);
307     }
308
309     /**
310      * Get index format version
311      *
312      * @return integer
313      */
314     public function getFormatVersion()
315     {
316         return $this->_formatVersion;
317     }
318
319     /**
320      * Set index format version.
321      * Index is converted to this format at the nearest upfdate time
322      *
323      * @param int $formatVersion
324      * @throws Zend_Search_Lucene_Exception
325      */
326     public function setFormatVersion($formatVersion)
327     {
328         if ($formatVersion != self::FORMAT_PRE_2_1  &&
329             $formatVersion != self::FORMAT_2_1  &&
330             $formatVersion != self::FORMAT_2_3) {
331             require_once 'Zend/Search/Lucene/Exception.php';
332             throw new Zend_Search_Lucene_Exception('Unsupported index format');
333         }
334
335         $this->_formatVersion = $formatVersion;
336     }
337
338     /**
339      * Read segments file for pre-2.1 Lucene index format
340      *
341      * @throws Zend_Search_Lucene_Exception
342      */
343     private function _readPre21SegmentsFile()
344     {
345         $segmentsFile = $this->_directory->getFileObject('segments');
346
347         $format = $segmentsFile->readInt();
348
349         if ($format != (int)0xFFFFFFFF) {
350             require_once 'Zend/Search/Lucene/Exception.php';
351             throw new Zend_Search_Lucene_Exception('Wrong segments file format');
352         }
353
354         // read version
355         $segmentsFile->readLong();
356
357         // read segment name counter
358         $segmentsFile->readInt();
359
360         $segments = $segmentsFile->readInt();
361
362         $this->_docCount = 0;
363
364         // read segmentInfos
365         for ($count = 0; $count < $segments; $count++) {
366             $segName = $segmentsFile->readString();
367             $segSize = $segmentsFile->readInt();
368             $this->_docCount += $segSize;
369
370             $this->_segmentInfos[$segName] =
371                                 new Zend_Search_Lucene_Index_SegmentInfo($this->_directory,
372                                                                          $segName,
373                                                                          $segSize);
374         }
375
376         // Use 2.1 as a target version. Index will be reorganized at update time.
377         $this->_formatVersion = self::FORMAT_2_1;
378     }
379
380     /**
381      * Read segments file
382      *
383      * @throws Zend_Search_Lucene_Exception
384      */
385     private function _readSegmentsFile()
386     {
387         $segmentsFile = $this->_directory->getFileObject(self::getSegmentFileName($this->_generation));
388
389         $format = $segmentsFile->readInt();
390
391         if ($format == (int)0xFFFFFFFC) {
392             $this->_formatVersion = self::FORMAT_2_3;
393         } else if ($format == (int)0xFFFFFFFD) {
394             $this->_formatVersion = self::FORMAT_2_1;
395         } else {
396             require_once 'Zend/Search/Lucene/Exception.php';
397             throw new Zend_Search_Lucene_Exception('Unsupported segments file format');
398         }
399
400         // read version
401         $segmentsFile->readLong();
402
403         // read segment name counter
404         $segmentsFile->readInt();
405
406         $segments = $segmentsFile->readInt();
407
408         $this->_docCount = 0;
409
410         // read segmentInfos
411         for ($count = 0; $count < $segments; $count++) {
412             $segName = $segmentsFile->readString();
413             $segSize = $segmentsFile->readInt();
414
415             // 2.1+ specific properties
416             $delGen = $segmentsFile->readLong();
417
418             if ($this->_formatVersion == self::FORMAT_2_3) {
419                 $docStoreOffset = $segmentsFile->readInt();
420
421                 if ($docStoreOffset != (int)0xFFFFFFFF) {
422                     $docStoreSegment        = $segmentsFile->readString();
423                     $docStoreIsCompoundFile = $segmentsFile->readByte();
424
425                     $docStoreOptions = array('offset'     => $docStoreOffset,
426                                              'segment'    => $docStoreSegment,
427                                              'isCompound' => ($docStoreIsCompoundFile == 1));
428                 } else {
429                     $docStoreOptions = null;
430                 }
431             } else {
432                 $docStoreOptions = null;
433             }
434
435             $hasSingleNormFile = $segmentsFile->readByte();
436             $numField          = $segmentsFile->readInt();
437
438             $normGens = array();
439             if ($numField != (int)0xFFFFFFFF) {
440                 for ($count1 = 0; $count1 < $numField; $count1++) {
441                     $normGens[] = $segmentsFile->readLong();
442                 }
443
444                 require_once 'Zend/Search/Lucene/Exception.php';
445                 throw new Zend_Search_Lucene_Exception('Separate norm files are not supported. Optimize index to use it with Zend_Search_Lucene.');
446             }
447
448             $isCompoundByte     = $segmentsFile->readByte();
449
450             if ($isCompoundByte == 0xFF) {
451                 // The segment is not a compound file
452                 $isCompound = false;
453             } else if ($isCompoundByte == 0x00) {
454                 // The status is unknown
455                 $isCompound = null;
456             } else if ($isCompoundByte == 0x01) {
457                 // The segment is a compound file
458                 $isCompound = true;
459             }
460
461             $this->_docCount += $segSize;
462
463             $this->_segmentInfos[$segName] =
464                                 new Zend_Search_Lucene_Index_SegmentInfo($this->_directory,
465                                                                          $segName,
466                                                                          $segSize,
467                                                                          $delGen,
468                                                                          $docStoreOptions,
469                                                                          $hasSingleNormFile,
470                                                                          $isCompound);
471         }
472     }
473
474     /**
475      * Opens the index.
476      *
477      * IndexReader constructor needs Directory as a parameter. It should be
478      * a string with a path to the index folder or a Directory object.
479      *
480      * @param mixed $directory
481      * @throws Zend_Search_Lucene_Exception
482      */
483     public function __construct($directory = null, $create = false)
484     {
485         if ($directory === null) {
486             require_once 'Zend/Search/Lucene/Exception.php';
487             throw new Zend_Search_Exception('No index directory specified');
488         }
489
490         if ($directory instanceof Zend_Search_Lucene_Storage_Directory_Filesystem) {
491             $this->_directory      = $directory;
492             $this->_closeDirOnExit = false;
493         } else {
494             $this->_directory      = new Zend_Search_Lucene_Storage_Directory_Filesystem($directory);
495             $this->_closeDirOnExit = true;
496         }
497
498         $this->_segmentInfos = array();
499
500         // Mark index as "under processing" to prevent other processes from premature index cleaning
501         Zend_Search_Lucene_LockManager::obtainReadLock($this->_directory);
502
503         $this->_generation = self::getActualGeneration($this->_directory);
504
505         if ($create) {
506             require_once 'Zend/Search/Lucene/Exception.php';
507             try {
508                 Zend_Search_Lucene_LockManager::obtainWriteLock($this->_directory);
509             } catch (Zend_Search_Lucene_Exception $e) {
510                 Zend_Search_Lucene_LockManager::releaseReadLock($this->_directory);
511
512                 if (strpos($e->getMessage(), 'Can\'t obtain exclusive index lock') === false) {
513                     throw $e;
514                 } else {
515                     throw new Zend_Search_Lucene_Exception('Can\'t create index. It\'s under processing now');
516                 }
517             }
518
519             if ($this->_generation == -1) {
520                 // Directory doesn't contain existing index, start from 1
521                 $this->_generation = 1;
522                 $nameCounter = 0;
523             } else {
524                 // Directory contains existing index
525                 $segmentsFile = $this->_directory->getFileObject(self::getSegmentFileName($this->_generation));
526                 $segmentsFile->seek(12); // 12 = 4 (int, file format marker) + 8 (long, index version)
527
528                 $nameCounter = $segmentsFile->readInt();
529                 $this->_generation++;
530             }
531
532             Zend_Search_Lucene_Index_Writer::createIndex($this->_directory, $this->_generation, $nameCounter);
533
534             Zend_Search_Lucene_LockManager::releaseWriteLock($this->_directory);
535         }
536
537         if ($this->_generation == -1) {
538             require_once 'Zend/Search/Lucene/Exception.php';
539             throw new Zend_Search_Lucene_Exception('Index doesn\'t exists in the specified directory.');
540         } else if ($this->_generation == 0) {
541             $this->_readPre21SegmentsFile();
542         } else {
543             $this->_readSegmentsFile();
544         }
545     }
546
547     /**
548      * Close current index and free resources
549      */
550     private function _close()
551     {
552         if ($this->_closed) {
553             // index is already closed and resources are cleaned up
554             return;
555         }
556
557         $this->commit();
558
559         // Release "under processing" flag
560         Zend_Search_Lucene_LockManager::releaseReadLock($this->_directory);
561
562         if ($this->_closeDirOnExit) {
563             $this->_directory->close();
564         }
565
566         $this->_directory    = null;
567         $this->_writer       = null;
568         $this->_segmentInfos = null;
569
570         $this->_closed = true;
571     }
572
573     /**
574      * Add reference to the index object
575      *
576      * @internal
577      */
578     public function addReference()
579     {
580         $this->_refCount++;
581     }
582
583     /**
584      * Remove reference from the index object
585      *
586      * When reference count becomes zero, index is closed and resources are cleaned up
587      *
588      * @internal
589      */
590     public function removeReference()
591     {
592         $this->_refCount--;
593
594         if ($this->_refCount == 0) {
595             $this->_close();
596         }
597     }
598
599     /**
600      * Object destructor
601      */
602     public function __destruct()
603     {
604         $this->_close();
605     }
606
607     /**
608      * Returns an instance of Zend_Search_Lucene_Index_Writer for the index
609      *
610      * @return Zend_Search_Lucene_Index_Writer
611      */
612     private function _getIndexWriter()
613     {
614         if (!$this->_writer instanceof Zend_Search_Lucene_Index_Writer) {
615             $this->_writer = new Zend_Search_Lucene_Index_Writer($this->_directory, $this->_segmentInfos, $this->_formatVersion);
616         }
617
618         return $this->_writer;
619     }
620
621
622     /**
623      * Returns the Zend_Search_Lucene_Storage_Directory instance for this index.
624      *
625      * @return Zend_Search_Lucene_Storage_Directory
626      */
627     public function getDirectory()
628     {
629         return $this->_directory;
630     }
631
632
633     /**
634      * Returns the total number of documents in this index (including deleted documents).
635      *
636      * @return integer
637      */
638     public function count()
639     {
640         return $this->_docCount;
641     }
642
643     /**
644      * Returns one greater than the largest possible document number.
645      * This may be used to, e.g., determine how big to allocate a structure which will have
646      * an element for every document number in an index.
647      *
648      * @return integer
649      */
650     public function maxDoc()
651     {
652         return $this->count();
653     }
654
655     /**
656      * Returns the total number of non-deleted documents in this index.
657      *
658      * @return integer
659      */
660     public function numDocs()
661     {
662         $numDocs = 0;
663
664         foreach ($this->_segmentInfos as $segmentInfo) {
665             $numDocs += $segmentInfo->numDocs();
666         }
667
668         return $numDocs;
669     }
670
671     /**
672      * Checks, that document is deleted
673      *
674      * @param integer $id
675      * @return boolean
676      * @throws Zend_Search_Lucene_Exception    Exception is thrown if $id is out of the range
677      */
678     public function isDeleted($id)
679     {
680         if ($id >= $this->_docCount) {
681             require_once 'Zend/Search/Lucene/Exception.php';
682             throw new Zend_Search_Lucene_Exception('Document id is out of the range.');
683         }
684
685         $segmentStartId = 0;
686         foreach ($this->_segmentInfos as $segmentInfo) {
687             if ($segmentStartId + $segmentInfo->count() > $id) {
688                 break;
689             }
690
691             $segmentStartId += $segmentInfo->count();
692         }
693
694         return $segmentInfo->isDeleted($id - $segmentStartId);
695     }
696
697     /**
698      * Set default search field.
699      *
700      * Null means, that search is performed through all fields by default
701      *
702      * Default value is null
703      *
704      * @param string $fieldName
705      */
706     public static function setDefaultSearchField($fieldName)
707     {
708         self::$_defaultSearchField = $fieldName;
709     }
710
711     /**
712      * Get default search field.
713      *
714      * Null means, that search is performed through all fields by default
715      *
716      * @return string
717      */
718     public static function getDefaultSearchField()
719     {
720         return self::$_defaultSearchField;
721     }
722
723     /**
724      * Set result set limit.
725      *
726      * 0 (default) means no limit
727      *
728      * @param integer $limit
729      */
730     public static function setResultSetLimit($limit)
731     {
732         self::$_resultSetLimit = $limit;
733     }
734
735     /**
736      * Get result set limit.
737      *
738      * 0 means no limit
739      *
740      * @return integer
741      */
742     public static function getResultSetLimit()
743     {
744         return self::$_resultSetLimit;
745     }
746
747     /**
748      * Set terms per query limit.
749      *
750      * 0 means no limit
751      *
752      * @param integer $limit
753      */
754     public static function setTermsPerQueryLimit($limit)
755     {
756         self::$_termsPerQueryLimit = $limit;
757     }
758
759     /**
760      * Get result set limit.
761      *
762      * 0 (default) means no limit
763      *
764      * @return integer
765      */
766     public static function getTermsPerQueryLimit()
767     {
768         return self::$_termsPerQueryLimit;
769     }
770
771     /**
772      * Retrieve index maxBufferedDocs option
773      *
774      * maxBufferedDocs is a minimal number of documents required before
775      * the buffered in-memory documents are written into a new Segment
776      *
777      * Default value is 10
778      *
779      * @return integer
780      */
781     public function getMaxBufferedDocs()
782     {
783         return $this->_getIndexWriter()->maxBufferedDocs;
784     }
785
786     /**
787      * Set index maxBufferedDocs option
788      *
789      * maxBufferedDocs is a minimal number of documents required before
790      * the buffered in-memory documents are written into a new Segment
791      *
792      * Default value is 10
793      *
794      * @param integer $maxBufferedDocs
795      */
796     public function setMaxBufferedDocs($maxBufferedDocs)
797     {
798         $this->_getIndexWriter()->maxBufferedDocs = $maxBufferedDocs;
799     }
800
801     /**
802      * Retrieve index maxMergeDocs option
803      *
804      * maxMergeDocs is a largest number of documents ever merged by addDocument().
805      * Small values (e.g., less than 10,000) are best for interactive indexing,
806      * as this limits the length of pauses while indexing to a few seconds.
807      * Larger values are best for batched indexing and speedier searches.
808      *
809      * Default value is PHP_INT_MAX
810      *
811      * @return integer
812      */
813     public function getMaxMergeDocs()
814     {
815         return $this->_getIndexWriter()->maxMergeDocs;
816     }
817
818     /**
819      * Set index maxMergeDocs option
820      *
821      * maxMergeDocs is a largest number of documents ever merged by addDocument().
822      * Small values (e.g., less than 10,000) are best for interactive indexing,
823      * as this limits the length of pauses while indexing to a few seconds.
824      * Larger values are best for batched indexing and speedier searches.
825      *
826      * Default value is PHP_INT_MAX
827      *
828      * @param integer $maxMergeDocs
829      */
830     public function setMaxMergeDocs($maxMergeDocs)
831     {
832         $this->_getIndexWriter()->maxMergeDocs = $maxMergeDocs;
833     }
834
835     /**
836      * Retrieve index mergeFactor option
837      *
838      * mergeFactor determines how often segment indices are merged by addDocument().
839      * With smaller values, less RAM is used while indexing,
840      * and searches on unoptimized indices are faster,
841      * but indexing speed is slower.
842      * With larger values, more RAM is used during indexing,
843      * and while searches on unoptimized indices are slower,
844      * indexing is faster.
845      * Thus larger values (> 10) are best for batch index creation,
846      * and smaller values (< 10) for indices that are interactively maintained.
847      *
848      * Default value is 10
849      *
850      * @return integer
851      */
852     public function getMergeFactor()
853     {
854         return $this->_getIndexWriter()->mergeFactor;
855     }
856
857     /**
858      * Set index mergeFactor option
859      *
860      * mergeFactor determines how often segment indices are merged by addDocument().
861      * With smaller values, less RAM is used while indexing,
862      * and searches on unoptimized indices are faster,
863      * but indexing speed is slower.
864      * With larger values, more RAM is used during indexing,
865      * and while searches on unoptimized indices are slower,
866      * indexing is faster.
867      * Thus larger values (> 10) are best for batch index creation,
868      * and smaller values (< 10) for indices that are interactively maintained.
869      *
870      * Default value is 10
871      *
872      * @param integer $maxMergeDocs
873      */
874     public function setMergeFactor($mergeFactor)
875     {
876         $this->_getIndexWriter()->mergeFactor = $mergeFactor;
877     }
878
879     /**
880      * Performs a query against the index and returns an array
881      * of Zend_Search_Lucene_Search_QueryHit objects.
882      * Input is a string or Zend_Search_Lucene_Search_Query.
883      *
884      * @param mixed $query
885      * @return array Zend_Search_Lucene_Search_QueryHit
886      * @throws Zend_Search_Lucene_Exception
887      */
888     public function find($query)
889     {
890         if (is_string($query)) {
891             $query = Zend_Search_Lucene_Search_QueryParser::parse($query);
892         }
893
894         if (!$query instanceof Zend_Search_Lucene_Search_Query) {
895             require_once 'Zend/Search/Lucene/Exception.php';
896             throw new Zend_Search_Lucene_Exception('Query must be a string or Zend_Search_Lucene_Search_Query object');
897         }
898
899         $this->commit();
900
901         $hits   = array();
902         $scores = array();
903         $ids    = array();
904
905         $query = $query->rewrite($this)->optimize($this);
906
907         $query->execute($this);
908
909         $topScore = 0;
910
911         foreach ($query->matchedDocs() as $id => $num) {
912             $docScore = $query->score($id, $this);
913             if( $docScore != 0 ) {
914                 $hit = new Zend_Search_Lucene_Search_QueryHit($this);
915                 $hit->id = $id;
916                 $hit->score = $docScore;
917
918                 $hits[]   = $hit;
919                 $ids[]    = $id;
920                 $scores[] = $docScore;
921
922                 if ($docScore > $topScore) {
923                     $topScore = $docScore;
924                 }
925             }
926
927             if (self::$_resultSetLimit != 0  &&  count($hits) >= self::$_resultSetLimit) {
928                 break;
929             }
930         }
931
932         if (count($hits) == 0) {
933             // skip sorting, which may cause a error on empty index
934             return array();
935         }
936
937         if ($topScore > 1) {
938             foreach ($hits as $hit) {
939                 $hit->score /= $topScore;
940             }
941         }
942
943         if (func_num_args() == 1) {
944             // sort by scores
945             array_multisort($scores, SORT_DESC, SORT_NUMERIC,
946                             $ids,    SORT_ASC,  SORT_NUMERIC,
947                             $hits);
948         } else {
949             // sort by given field names
950
951             $argList    = func_get_args();
952             $fieldNames = $this->getFieldNames();
953             $sortArgs   = array();
954
955             // PHP 5.3 now expects all arguments to array_multisort be passed by
956             // reference; since constants can't be passed by reference, create 
957             // some placeholder variables.
958             $sortReg    = SORT_REGULAR;
959             $sortAsc    = SORT_ASC;
960             $sortNum    = SORT_NUMERIC;
961
962             require_once 'Zend/Search/Lucene/Exception.php';
963             for ($count = 1; $count < count($argList); $count++) {
964                 $fieldName = $argList[$count];
965
966                 if (!is_string($fieldName)) {
967                     throw new Zend_Search_Lucene_Exception('Field name must be a string.');
968                 }
969
970                 if (!in_array($fieldName, $fieldNames)) {
971                     throw new Zend_Search_Lucene_Exception('Wrong field name.');
972                 }
973
974                 $valuesArray = array();
975                 foreach ($hits as $hit) {
976                     try {
977                         $value = $hit->getDocument()->getFieldValue($fieldName);
978                     } catch (Zend_Search_Lucene_Exception $e) {
979                         if (strpos($e->getMessage(), 'not found') === false) {
980                             throw $e;
981                         } else {
982                             $value = null;
983                         }
984                     }
985
986                     $valuesArray[] = $value;
987                 }
988
989                 $sortArgs[] = &$valuesArray;
990
991                 if ($count + 1 < count($argList)  &&  is_integer($argList[$count+1])) {
992                     $count++;
993                     $sortArgs[] = &$argList[$count];
994
995                     if ($count + 1 < count($argList)  &&  is_integer($argList[$count+1])) {
996                         $count++;
997                         $sortArgs[] = &$argList[$count];
998                     } else {
999                         if ($argList[$count] == SORT_ASC  || $argList[$count] == SORT_DESC) {
1000                             $sortArgs[] = &$sortReg;
1001                         } else {
1002                             $sortArgs[] = &$sortAsc;
1003                         }
1004                     }
1005                 } else {
1006                     $sortArgs[] = &$sortAsc;
1007                     $sortArgs[] = &$sortReg;
1008                 }
1009             }
1010
1011             // Sort by id's if values are equal
1012             $sortArgs[] = &$ids;
1013             $sortArgs[] = &$sortAsc;
1014             $sortArgs[] = &$sortNum;
1015
1016             // Array to be sorted
1017             $sortArgs[] = &$hits;
1018
1019             // Do sort
1020             call_user_func_array('array_multisort', $sortArgs);
1021         }
1022
1023         return $hits;
1024     }
1025
1026
1027     /**
1028      * Returns a list of all unique field names that exist in this index.
1029      *
1030      * @param boolean $indexed
1031      * @return array
1032      */
1033     public function getFieldNames($indexed = false)
1034     {
1035         $result = array();
1036         foreach( $this->_segmentInfos as $segmentInfo ) {
1037             $result = array_merge($result, $segmentInfo->getFields($indexed));
1038         }
1039         return $result;
1040     }
1041
1042
1043     /**
1044      * Returns a Zend_Search_Lucene_Document object for the document
1045      * number $id in this index.
1046      *
1047      * @param integer|Zend_Search_Lucene_Search_QueryHit $id
1048      * @return Zend_Search_Lucene_Document
1049      * @throws Zend_Search_Lucene_Exception    Exception is thrown if $id is out of the range
1050      */
1051     public function getDocument($id)
1052     {
1053         if ($id instanceof Zend_Search_Lucene_Search_QueryHit) {
1054             /* @var $id Zend_Search_Lucene_Search_QueryHit */
1055             $id = $id->id;
1056         }
1057
1058         if ($id >= $this->_docCount) {
1059             require_once 'Zend/Search/Lucene/Exception.php';
1060             throw new Zend_Search_Lucene_Exception('Document id is out of the range.');
1061         }
1062
1063         $segmentStartId = 0;
1064         foreach ($this->_segmentInfos as $segmentInfo) {
1065             if ($segmentStartId + $segmentInfo->count() > $id) {
1066                 break;
1067             }
1068
1069             $segmentStartId += $segmentInfo->count();
1070         }
1071
1072         $fdxFile = $segmentInfo->openCompoundFile('.fdx');
1073         $fdxFile->seek(($id-$segmentStartId)*8, SEEK_CUR);
1074         $fieldValuesPosition = $fdxFile->readLong();
1075
1076         $fdtFile = $segmentInfo->openCompoundFile('.fdt');
1077         $fdtFile->seek($fieldValuesPosition, SEEK_CUR);
1078         $fieldCount = $fdtFile->readVInt();
1079
1080         $doc = new Zend_Search_Lucene_Document();
1081         for ($count = 0; $count < $fieldCount; $count++) {
1082             $fieldNum = $fdtFile->readVInt();
1083             $bits = $fdtFile->readByte();
1084
1085             $fieldInfo = $segmentInfo->getField($fieldNum);
1086
1087             if (!($bits & 2)) { // Text data
1088                 $field = new Zend_Search_Lucene_Field($fieldInfo->name,
1089                                                       $fdtFile->readString(),
1090                                                       'UTF-8',
1091                                                       true,
1092                                                       $fieldInfo->isIndexed,
1093                                                       $bits & 1 );
1094             } else {            // Binary data
1095                 $field = new Zend_Search_Lucene_Field($fieldInfo->name,
1096                                                       $fdtFile->readBinary(),
1097                                                       '',
1098                                                       true,
1099                                                       $fieldInfo->isIndexed,
1100                                                       $bits & 1,
1101                                                       true );
1102             }
1103
1104             $doc->addField($field);
1105         }
1106
1107         return $doc;
1108     }
1109
1110
1111     /**
1112      * Returns true if index contain documents with specified term.
1113      *
1114      * Is used for query optimization.
1115      *
1116      * @param Zend_Search_Lucene_Index_Term $term
1117      * @return boolean
1118      */
1119     public function hasTerm(Zend_Search_Lucene_Index_Term $term)
1120     {
1121         foreach ($this->_segmentInfos as $segInfo) {
1122             if ($segInfo->getTermInfo($term) instanceof Zend_Search_Lucene_Index_TermInfo) {
1123                 return true;
1124             }
1125         }
1126
1127         return false;
1128     }
1129
1130     /**
1131      * Returns IDs of all documents containing term.
1132      *
1133      * @param Zend_Search_Lucene_Index_Term $term
1134      * @param Zend_Search_Lucene_Index_DocsFilter|null $docsFilter
1135      * @return array
1136      */
1137     public function termDocs(Zend_Search_Lucene_Index_Term $term, $docsFilter = null)
1138     {
1139         $subResults = array();
1140         $segmentStartDocId = 0;
1141
1142         foreach ($this->_segmentInfos as $segmentInfo) {
1143             $subResults[] = $segmentInfo->termDocs($term, $segmentStartDocId, $docsFilter);
1144
1145             $segmentStartDocId += $segmentInfo->count();
1146         }
1147
1148         if (count($subResults) == 0) {
1149             return array();
1150         } else if (count($subResults) == 0) {
1151             // Index is optimized (only one segment)
1152             // Do not perform array reindexing
1153             return reset($subResults);
1154         } else {
1155             $result = call_user_func_array('array_merge', $subResults);
1156         }
1157
1158         return $result;
1159     }
1160
1161     /**
1162      * Returns documents filter for all documents containing term.
1163      *
1164      * It performs the same operation as termDocs, but return result as
1165      * Zend_Search_Lucene_Index_DocsFilter object
1166      *
1167      * @param Zend_Search_Lucene_Index_Term $term
1168      * @param Zend_Search_Lucene_Index_DocsFilter|null $docsFilter
1169      * @return Zend_Search_Lucene_Index_DocsFilter
1170      */
1171     public function termDocsFilter(Zend_Search_Lucene_Index_Term $term, $docsFilter = null)
1172     {
1173         $segmentStartDocId = 0;
1174         $result = new Zend_Search_Lucene_Index_DocsFilter();
1175
1176         foreach ($this->_segmentInfos as $segmentInfo) {
1177             $subResults[] = $segmentInfo->termDocs($term, $segmentStartDocId, $docsFilter);
1178
1179             $segmentStartDocId += $segmentInfo->count();
1180         }
1181
1182         if (count($subResults) == 0) {
1183             return array();
1184         } else if (count($subResults) == 0) {
1185             // Index is optimized (only one segment)
1186             // Do not perform array reindexing
1187             return reset($subResults);
1188         } else {
1189             $result = call_user_func_array('array_merge', $subResults);
1190         }
1191
1192         return $result;
1193     }
1194
1195
1196     /**
1197      * Returns an array of all term freqs.
1198      * Result array structure: array(docId => freq, ...)
1199      *
1200      * @param Zend_Search_Lucene_Index_Term $term
1201      * @param Zend_Search_Lucene_Index_DocsFilter|null $docsFilter
1202      * @return integer
1203      */
1204     public function termFreqs(Zend_Search_Lucene_Index_Term $term, $docsFilter = null)
1205     {
1206         $result = array();
1207         $segmentStartDocId = 0;
1208         foreach ($this->_segmentInfos as $segmentInfo) {
1209             $result += $segmentInfo->termFreqs($term, $segmentStartDocId, $docsFilter);
1210
1211             $segmentStartDocId += $segmentInfo->count();
1212         }
1213
1214         return $result;
1215     }
1216
1217     /**
1218      * Returns an array of all term positions in the documents.
1219      * Result array structure: array(docId => array(pos1, pos2, ...), ...)
1220      *
1221      * @param Zend_Search_Lucene_Index_Term $term
1222      * @param Zend_Search_Lucene_Index_DocsFilter|null $docsFilter
1223      * @return array
1224      */
1225     public function termPositions(Zend_Search_Lucene_Index_Term $term, $docsFilter = null)
1226     {
1227         $result = array();
1228         $segmentStartDocId = 0;
1229         foreach ($this->_segmentInfos as $segmentInfo) {
1230             $result += $segmentInfo->termPositions($term, $segmentStartDocId, $docsFilter);
1231
1232             $segmentStartDocId += $segmentInfo->count();
1233         }
1234
1235         return $result;
1236     }
1237
1238
1239     /**
1240      * Returns the number of documents in this index containing the $term.
1241      *
1242      * @param Zend_Search_Lucene_Index_Term $term
1243      * @return integer
1244      */
1245     public function docFreq(Zend_Search_Lucene_Index_Term $term)
1246     {
1247         $result = 0;
1248         foreach ($this->_segmentInfos as $segInfo) {
1249             $termInfo = $segInfo->getTermInfo($term);
1250             if ($termInfo !== null) {
1251                 $result += $termInfo->docFreq;
1252             }
1253         }
1254
1255         return $result;
1256     }
1257
1258
1259     /**
1260      * Retrive similarity used by index reader
1261      *
1262      * @return Zend_Search_Lucene_Search_Similarity
1263      */
1264     public function getSimilarity()
1265     {
1266         return Zend_Search_Lucene_Search_Similarity::getDefault();
1267     }
1268
1269
1270     /**
1271      * Returns a normalization factor for "field, document" pair.
1272      *
1273      * @param integer $id
1274      * @param string $fieldName
1275      * @return float
1276      */
1277     public function norm($id, $fieldName)
1278     {
1279         if ($id >= $this->_docCount) {
1280             return null;
1281         }
1282
1283         $segmentStartId = 0;
1284         foreach ($this->_segmentInfos as $segInfo) {
1285             if ($segmentStartId + $segInfo->count() > $id) {
1286                 break;
1287             }
1288
1289             $segmentStartId += $segInfo->count();
1290         }
1291
1292         if ($segInfo->isDeleted($id - $segmentStartId)) {
1293             return 0;
1294         }
1295
1296         return $segInfo->norm($id - $segmentStartId, $fieldName);
1297     }
1298
1299     /**
1300      * Returns true if any documents have been deleted from this index.
1301      *
1302      * @return boolean
1303      */
1304     public function hasDeletions()
1305     {
1306         foreach ($this->_segmentInfos as $segmentInfo) {
1307             if ($segmentInfo->hasDeletions()) {
1308                 return true;
1309             }
1310         }
1311
1312         return false;
1313     }
1314
1315
1316     /**
1317      * Deletes a document from the index.
1318      * $id is an internal document id
1319      *
1320      * @param integer|Zend_Search_Lucene_Search_QueryHit $id
1321      * @throws Zend_Search_Lucene_Exception
1322      */
1323     public function delete($id)
1324     {
1325         if ($id instanceof Zend_Search_Lucene_Search_QueryHit) {
1326             /* @var $id Zend_Search_Lucene_Search_QueryHit */
1327             $id = $id->id;
1328         }
1329
1330         if ($id >= $this->_docCount) {
1331             require_once 'Zend/Search/Lucene/Exception.php';
1332             throw new Zend_Search_Lucene_Exception('Document id is out of the range.');
1333         }
1334
1335         $segmentStartId = 0;
1336         foreach ($this->_segmentInfos as $segmentInfo) {
1337             if ($segmentStartId + $segmentInfo->count() > $id) {
1338                 break;
1339             }
1340
1341             $segmentStartId += $segmentInfo->count();
1342         }
1343         $segmentInfo->delete($id - $segmentStartId);
1344
1345         $this->_hasChanges = true;
1346     }
1347
1348
1349
1350     /**
1351      * Adds a document to this index.
1352      *
1353      * @param Zend_Search_Lucene_Document $document
1354      */
1355     public function addDocument(Zend_Search_Lucene_Document $document)
1356     {
1357         $this->_getIndexWriter()->addDocument($document);
1358         $this->_docCount++;
1359
1360         $this->_hasChanges = true;
1361     }
1362
1363
1364     /**
1365      * Update document counter
1366      */
1367     private function _updateDocCount()
1368     {
1369         $this->_docCount = 0;
1370         foreach ($this->_segmentInfos as $segInfo) {
1371             $this->_docCount += $segInfo->count();
1372         }
1373     }
1374
1375     /**
1376      * Commit changes resulting from delete() or undeleteAll() operations.
1377      *
1378      * @todo undeleteAll processing.
1379      */
1380     public function commit()
1381     {
1382         if ($this->_hasChanges) {
1383             $this->_getIndexWriter()->commit();
1384
1385             $this->_updateDocCount();
1386
1387             $this->_hasChanges = false;
1388         }
1389     }
1390
1391
1392     /**
1393      * Optimize index.
1394      *
1395      * Merges all segments into one
1396      */
1397     public function optimize()
1398     {
1399         // Commit changes if any changes have been made
1400         $this->commit();
1401
1402         if (count($this->_segmentInfos) > 1 || $this->hasDeletions()) {
1403             $this->_getIndexWriter()->optimize();
1404             $this->_updateDocCount();
1405         }
1406     }
1407
1408
1409     /**
1410      * Returns an array of all terms in this index.
1411      *
1412      * @return array
1413      */
1414     public function terms()
1415     {
1416         $result = array();
1417
1418         $segmentInfoQueue = new Zend_Search_Lucene_Index_TermsPriorityQueue();
1419
1420         foreach ($this->_segmentInfos as $segmentInfo) {
1421             $segmentInfo->resetTermsStream();
1422
1423             // Skip "empty" segments
1424             if ($segmentInfo->currentTerm() !== null) {
1425                 $segmentInfoQueue->put($segmentInfo);
1426             }
1427         }
1428
1429         while (($segmentInfo = $segmentInfoQueue->pop()) !== null) {
1430             if ($segmentInfoQueue->top() === null ||
1431                 $segmentInfoQueue->top()->currentTerm()->key() !=
1432                             $segmentInfo->currentTerm()->key()) {
1433                 // We got new term
1434                 $result[] = $segmentInfo->currentTerm();
1435             }
1436
1437             if ($segmentInfo->nextTerm() !== null) {
1438                 // Put segment back into the priority queue
1439                 $segmentInfoQueue->put($segmentInfo);
1440             }
1441         }
1442
1443         return $result;
1444     }
1445
1446
1447     /**
1448      * Terms stream priority queue object
1449      *
1450      * @var Zend_Search_Lucene_TermStreamsPriorityQueue
1451      */
1452     private $_termsStream = null;
1453
1454     /**
1455      * Reset terms stream.
1456      */
1457     public function resetTermsStream()
1458     {
1459         if ($this->_termsStream === null) {
1460             $this->_termsStream = new Zend_Search_Lucene_TermStreamsPriorityQueue($this->_segmentInfos);
1461         } else {
1462                 $this->_termsStream->resetTermsStream();
1463         }
1464     }
1465
1466     /**
1467      * Skip terms stream up to specified term preffix.
1468      *
1469      * Prefix contains fully specified field info and portion of searched term
1470      *
1471      * @param Zend_Search_Lucene_Index_Term $prefix
1472      */
1473     public function skipTo(Zend_Search_Lucene_Index_Term $prefix)
1474     {
1475         $this->_termsStream->skipTo($prefix);
1476     }
1477
1478     /**
1479      * Scans terms dictionary and returns next term
1480      *
1481      * @return Zend_Search_Lucene_Index_Term|null
1482      */
1483     public function nextTerm()
1484     {
1485         return $this->_termsStream->nextTerm();
1486     }
1487
1488     /**
1489      * Returns term in current position
1490      *
1491      * @return Zend_Search_Lucene_Index_Term|null
1492      */
1493     public function currentTerm()
1494     {
1495         return $this->_termsStream->currentTerm();
1496     }
1497
1498     /**
1499      * Close terms stream
1500      *
1501      * Should be used for resources clean up if stream is not read up to the end
1502      */
1503     public function closeTermsStream()
1504     {
1505         $this->_termsStream->closeTermsStream();
1506         $this->_termsStream = null;
1507     }
1508
1509
1510     /*************************************************************************
1511     @todo UNIMPLEMENTED
1512     *************************************************************************/
1513     /**
1514      * Undeletes all documents currently marked as deleted in this index.
1515      *
1516      * @todo Implementation
1517      */
1518     public function undeleteAll()
1519     {}
1520 }