View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.search.lucene;
25  
26  import java.io.IOException;
27  import java.io.StringReader;
28  import java.math.BigDecimal;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.Collections;
32  import java.util.HashSet;
33  import java.util.LinkedList;
34  import java.util.List;
35  import java.util.Set;
36  import java.util.UUID;
37  import java.util.regex.Pattern;
38  import net.jcip.annotations.NotThreadSafe;
39  import org.apache.lucene.analysis.Analyzer;
40  import org.apache.lucene.analysis.TokenStream;
41  import org.apache.lucene.analysis.tokenattributes.TermAttribute;
42  import org.apache.lucene.document.Document;
43  import org.apache.lucene.document.Field;
44  import org.apache.lucene.document.FieldSelector;
45  import org.apache.lucene.document.FieldSelectorResult;
46  import org.apache.lucene.document.NumericField;
47  import org.apache.lucene.document.Field.Index;
48  import org.apache.lucene.document.Field.Store;
49  import org.apache.lucene.index.IndexReader;
50  import org.apache.lucene.index.IndexWriter;
51  import org.apache.lucene.index.Term;
52  import org.apache.lucene.index.IndexWriter.MaxFieldLength;
53  import org.apache.lucene.queryParser.ParseException;
54  import org.apache.lucene.queryParser.QueryParser;
55  import org.apache.lucene.search.BooleanQuery;
56  import org.apache.lucene.search.IndexSearcher;
57  import org.apache.lucene.search.MatchAllDocsQuery;
58  import org.apache.lucene.search.NumericRangeQuery;
59  import org.apache.lucene.search.PrefixQuery;
60  import org.apache.lucene.search.Query;
61  import org.apache.lucene.search.ScoreDoc;
62  import org.apache.lucene.search.Scorer;
63  import org.apache.lucene.search.TermQuery;
64  import org.apache.lucene.search.TopDocs;
65  import org.apache.lucene.search.BooleanClause.Occur;
66  import org.apache.lucene.search.regex.JavaUtilRegexCapabilities;
67  import org.apache.lucene.search.regex.RegexQuery;
68  import org.apache.lucene.store.Directory;
69  import org.apache.lucene.util.Version;
70  import org.modeshape.common.util.Logger;
71  import org.modeshape.graph.JcrLexicon;
72  import org.modeshape.graph.Location;
73  import org.modeshape.graph.ModeShapeIntLexicon;
74  import org.modeshape.graph.ModeShapeLexicon;
75  import org.modeshape.graph.ModeShapeIntLexicon.Namespace;
76  import org.modeshape.graph.property.DateTime;
77  import org.modeshape.graph.property.Name;
78  import org.modeshape.graph.property.Path;
79  import org.modeshape.graph.property.Property;
80  import org.modeshape.graph.property.ValueFactories;
81  import org.modeshape.graph.property.ValueFactory;
82  import org.modeshape.graph.property.basic.BasicName;
83  import org.modeshape.graph.query.QueryResults.Columns;
84  import org.modeshape.graph.query.QueryResults.Statistics;
85  import org.modeshape.graph.query.model.Length;
86  import org.modeshape.graph.query.model.NodeDepth;
87  import org.modeshape.graph.query.model.NodeLocalName;
88  import org.modeshape.graph.query.model.NodeName;
89  import org.modeshape.graph.query.model.NodePath;
90  import org.modeshape.graph.query.model.Operator;
91  import org.modeshape.graph.query.model.PropertyValue;
92  import org.modeshape.graph.query.model.ReferenceValue;
93  import org.modeshape.search.lucene.AbstractLuceneSearchEngine.TupleCollector;
94  import org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession;
95  import org.modeshape.search.lucene.IndexRules.FieldType;
96  import org.modeshape.search.lucene.IndexRules.NumericRule;
97  import org.modeshape.search.lucene.IndexRules.Rule;
98  import org.modeshape.search.lucene.LuceneSearchWorkspace.ContentIndex;
99  import org.modeshape.search.lucene.query.CompareLengthQuery;
100 import org.modeshape.search.lucene.query.CompareNameQuery;
101 import org.modeshape.search.lucene.query.ComparePathQuery;
102 import org.modeshape.search.lucene.query.CompareStringQuery;
103 import org.modeshape.search.lucene.query.MatchNoneQuery;
104 import org.modeshape.search.lucene.query.NotQuery;
105 
106 /**
107  * The {@link WorkspaceSession} implementation for the {@link LuceneSearchEngine}.
108  */
109 @NotThreadSafe
110 public class LuceneSearchSession implements WorkspaceSession {
111 
112     protected static final Set<Name> NON_SEARCHABLE_NAMES = Collections.unmodifiableSet(new HashSet<Name>(
113                                                                                                           Arrays.asList(JcrLexicon.UUID,
114                                                                                                                         ModeShapeLexicon.UUID,
115                                                                                                                         JcrLexicon.PRIMARY_TYPE,
116                                                                                                                         JcrLexicon.MIXIN_TYPES,
117                                                                                                                         ModeShapeIntLexicon.NODE_DEFINITON,
118                                                                                                                         new BasicName(
119                                                                                                                                       Namespace.URI,
120                                                                                                                                       "multiValuedProperties"))));
121 
122     /**
123      * An immutable {@link FieldSelector} instance that accesses the UUID field.
124      */
125     protected static final FieldSelector LOCATION_FIELDS_SELECTOR = new FieldSelector() {
126         private static final long serialVersionUID = 1L;
127 
128         public FieldSelectorResult accept( String fieldName ) {
129             if (ContentIndex.PATH.equals(fieldName) || ContentIndex.LOCATION_ID_PROPERTIES.equals(fieldName)) {
130                 return FieldSelectorResult.LOAD;
131             }
132             return FieldSelectorResult.NO_LOAD;
133         }
134     };
135 
136     protected static final int MIN_DEPTH = 0;
137     protected static final int MAX_DEPTH = 100;
138     protected static final int MIN_SNS_INDEX = 1;
139     protected static final int MAX_SNS_INDEX = 1000; // assume there won't be more than 1000 same-name-siblings
140 
141     private final LuceneSearchWorkspace workspace;
142     protected final LuceneSearchProcessor processor;
143     private final Directory contentIndexDirectory;
144     private IndexReader contentReader;
145     private IndexWriter contentWriter;
146     private IndexSearcher contentSearcher;
147     private int numChanges;
148     private final Logger logger = Logger.getLogger(getClass());
149 
150     protected LuceneSearchSession( LuceneSearchWorkspace workspace,
151                                    LuceneSearchProcessor processor ) {
152         assert workspace != null;
153         assert processor != null;
154         this.workspace = workspace;
155         this.contentIndexDirectory = workspace.contentDirectory;
156         this.processor = processor;
157     }
158 
159     /**
160      * {@inheritDoc}
161      * 
162      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#getWorkspaceName()
163      */
164     public String getWorkspaceName() {
165         return workspace.getWorkspaceName();
166     }
167 
168     /**
169      * @return workspace
170      */
171     public LuceneSearchWorkspace getWorkspace() {
172         return workspace;
173     }
174 
175     protected IndexReader getContentReader() throws IOException {
176         if (contentReader == null) {
177             try {
178                 contentReader = IndexReader.open(contentIndexDirectory, processor.readOnly);
179             } catch (IOException e) {
180                 // try creating the workspace ...
181                 IndexWriter writer = new IndexWriter(contentIndexDirectory, workspace.analyzer, MaxFieldLength.UNLIMITED);
182                 writer.close();
183                 // And try reading again ...
184                 contentReader = IndexReader.open(contentIndexDirectory, processor.readOnly);
185             }
186         }
187         return contentReader;
188     }
189 
190     protected IndexWriter getContentWriter() throws IOException {
191         assert !processor.readOnly;
192         if (contentWriter == null) {
193             // Don't overwrite, but create if missing ...
194             contentWriter = new IndexWriter(contentIndexDirectory, workspace.analyzer, MaxFieldLength.UNLIMITED);
195         }
196         return contentWriter;
197     }
198 
199     public IndexSearcher getContentSearcher() throws IOException {
200         if (contentSearcher == null) {
201             contentSearcher = new IndexSearcher(getContentReader());
202         }
203         return contentSearcher;
204     }
205 
206     /**
207      * {@inheritDoc}
208      * 
209      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#getAnalyzer()
210      */
211     public Analyzer getAnalyzer() {
212         return workspace.analyzer;
213     }
214 
215     public boolean hasWriters() {
216         return contentWriter != null;
217     }
218 
219     protected final void recordChange() {
220         ++numChanges;
221     }
222 
223     protected final void recordChanges( int numberOfChanges ) {
224         assert numberOfChanges >= 0;
225         numChanges += numberOfChanges;
226     }
227 
228     /**
229      * {@inheritDoc}
230      * 
231      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#getChangeCount()
232      */
233     public final int getChangeCount() {
234         return numChanges;
235     }
236 
237     /**
238      * {@inheritDoc}
239      * 
240      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#commit()
241      */
242     public void commit() {
243         if (logger.isTraceEnabled() && numChanges > 0) {
244             logger.trace("index for \"{0}\" workspace: COMMIT", workspace.getWorkspaceName());
245         }
246 
247         // Is optimization required ...
248         final boolean optimize = workspace.isOptimizationRequired(numChanges);
249         numChanges = 0;
250 
251         IOException ioError = null;
252         RuntimeException runtimeError = null;
253         if (contentReader != null) {
254             try {
255                 contentReader.close();
256             } catch (IOException e) {
257                 ioError = e;
258             } catch (RuntimeException e) {
259                 runtimeError = e;
260             } finally {
261                 contentReader = null;
262             }
263         }
264         if (contentWriter != null) {
265             try {
266                 if (optimize) contentWriter.optimize();
267             } catch (IOException e) {
268                 if (ioError == null) ioError = e;
269             } catch (RuntimeException e) {
270                 if (runtimeError == null) runtimeError = e;
271             } finally {
272                 try {
273                     contentWriter.close();
274                 } catch (IOException e) {
275                     if (ioError == null) ioError = e;
276                 } catch (RuntimeException e) {
277                     if (runtimeError == null) runtimeError = e;
278                 } finally {
279                     contentWriter = null;
280                 }
281             }
282         }
283         if (ioError != null) {
284             String msg = LuceneI18n.errorWhileCommittingIndexChanges.text(workspace.getWorkspaceName(),
285                                                                           processor.getSourceName(),
286                                                                           ioError.getMessage());
287             throw new LuceneException(msg, ioError);
288         }
289         if (runtimeError != null) throw runtimeError;
290     }
291 
292     /**
293      * {@inheritDoc}
294      * 
295      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#rollback()
296      */
297     public void rollback() {
298         if (logger.isTraceEnabled() && numChanges > 0) {
299             logger.trace("index for \"{0}\" workspace: ROLLBACK", workspace.getWorkspaceName());
300         }
301         numChanges = 0;
302         IOException ioError = null;
303         RuntimeException runtimeError = null;
304         if (contentReader != null) {
305             try {
306                 contentReader.close();
307             } catch (IOException e) {
308                 ioError = e;
309             } catch (RuntimeException e) {
310                 runtimeError = e;
311             } finally {
312                 contentReader = null;
313             }
314         }
315         if (contentWriter != null) {
316             try {
317                 contentWriter.rollback();
318             } catch (IOException e) {
319                 if (ioError == null) ioError = e;
320             } catch (RuntimeException e) {
321                 if (runtimeError == null) runtimeError = e;
322             } finally {
323                 try {
324                     contentWriter.close();
325                 } catch (IOException e) {
326                     ioError = e;
327                 } catch (RuntimeException e) {
328                     runtimeError = e;
329                 } finally {
330                     contentWriter = null;
331                 }
332             }
333         }
334         if (ioError != null) {
335             String msg = LuceneI18n.errorWhileRollingBackIndexChanges.text(workspace.getWorkspaceName(),
336                                                                            processor.getSourceName(),
337                                                                            ioError.getMessage());
338             throw new LuceneException(msg, ioError);
339         }
340         if (runtimeError != null) throw runtimeError;
341     }
342 
343     protected Statistics search( String fullTextSearchExpression,
344                                  List<Object[]> results,
345                                  int maxRows,
346                                  int offset ) throws ParseException, IOException {
347         // Parse the full-text search and search against the 'fts' field ...
348         long planningNanos = System.nanoTime();
349         QueryParser parser = new QueryParser(Version.LUCENE_29, ContentIndex.FULL_TEXT, workspace.analyzer);
350         Query query = parser.parse(fullTextSearchExpression);
351         planningNanos = System.nanoTime() - planningNanos;
352 
353         // Execute the search and place the results into the supplied list ...
354         TopDocs docs = getContentSearcher().search(query, maxRows + offset);
355         IndexReader contentReader = getContentReader();
356         ScoreDoc[] scoreDocs = docs.scoreDocs;
357         int numberOfResults = scoreDocs.length;
358         if (numberOfResults > offset) {
359             // There are enough results to satisfy the offset ...
360             for (int i = offset, num = scoreDocs.length; i != num; ++i) {
361                 ScoreDoc result = scoreDocs[i];
362                 int docId = result.doc;
363                 // Find the UUID of the node (this UUID might be artificial, so we have to find the path) ...
364                 Document doc = contentReader.document(docId, LOCATION_FIELDS_SELECTOR);
365                 Location location = readLocation(doc);
366                 // Now add the location ...
367                 results.add(new Object[] {location, result.score});
368             }
369         }
370         long executionNanos = System.nanoTime() - planningNanos;
371         return new Statistics(planningNanos, 0L, 0L, executionNanos);
372     }
373 
374     protected Location readLocation( Document doc ) {
375         // Read the path ...
376         String pathString = doc.get(ContentIndex.PATH);
377         Path path = processor.pathFactory.create(pathString);
378         // Look for the Location's ID properties ...
379         String[] idProps = doc.getValues(ContentIndex.LOCATION_ID_PROPERTIES);
380         if (idProps.length == 0) {
381             return Location.create(path);
382         }
383         if (idProps.length == 1) {
384             Property idProp = processor.deserializeProperty(idProps[0]);
385             if (idProp == null) return Location.create(path);
386             if (idProp.isSingle() && (idProp.getName().equals(JcrLexicon.UUID) || idProp.getName().equals(ModeShapeLexicon.UUID))) {
387                 return Location.create(path, (UUID)idProp.getFirstValue()); // know that deserialize returns UUID value
388             }
389             return Location.create(path, idProp);
390         }
391         List<Property> properties = new LinkedList<Property>();
392         for (String idProp : idProps) {
393             Property prop = processor.deserializeProperty(idProp);
394             if (prop != null) properties.add(prop);
395         }
396         return properties.isEmpty() ? Location.create(path) : Location.create(path, properties);
397     }
398 
399     protected void setOrReplaceProperties( Location location,
400                                            Iterable<Property> properties ) throws IOException {
401         // Create the document for the content (properties) ...
402         Document doc = new Document();
403 
404         // Add the information every node has ...
405         Path path = location.getPath();
406         String pathStr = processor.pathAsString(path);
407         String nameStr = path.isRoot() ? "" : processor.stringFactory.create(path.getLastSegment().getName());
408         String localNameStr = path.isRoot() ? "" : path.getLastSegment().getName().getLocalName();
409         int sns = path.isRoot() ? 1 : path.getLastSegment().getIndex();
410 
411         // Create a separate document for the path, which makes it easier to handle moves since the path can
412         // be changed without changing any other content fields ...
413         doc.add(new Field(ContentIndex.PATH, pathStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
414         doc.add(new Field(ContentIndex.NODE_NAME, nameStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
415         doc.add(new Field(ContentIndex.LOCAL_NAME, localNameStr, Field.Store.YES, Field.Index.NOT_ANALYZED));
416         doc.add(new NumericField(ContentIndex.SNS_INDEX, Field.Store.YES, true).setIntValue(sns));
417         doc.add(new NumericField(ContentIndex.DEPTH, Field.Store.YES, true).setIntValue(path.size()));
418         if (location.hasIdProperties()) {
419             for (Property idProp : location.getIdProperties()) {
420                 String fieldValue = processor.serializeProperty(idProp);
421                 doc.add(new Field(ContentIndex.LOCATION_ID_PROPERTIES, fieldValue, Field.Store.YES, Field.Index.NOT_ANALYZED));
422             }
423         }
424 
425         // Always include the local name in the full-text search field ...
426         StringBuilder fullTextSearchValue = new StringBuilder();
427         fullTextSearchValue.append(localNameStr);
428 
429         // Index the properties
430         String stringValue = null;
431         for (Property property : properties) {
432             Name name = property.getName();
433             Rule rule = workspace.rules.getRule(name);
434             if (rule.isSkipped()) continue;
435             String nameString = processor.stringFactory.create(name);
436             FieldType type = rule.getType();
437             if (type == FieldType.DATE) {
438                 boolean index = rule.getIndexOption() != Field.Index.NO;
439                 for (Object value : property) {
440                     if (value == null) continue;
441                     // Add a separate field for each property value ...
442                     DateTime dateValue = processor.dateFactory.create(value);
443                     long longValue = dateValue.getMillisecondsInUtc();
444                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setLongValue(longValue));
445                 }
446                 continue;
447             }
448             if (type == FieldType.INT) {
449                 ValueFactory<Long> longFactory = processor.valueFactories.getLongFactory();
450                 boolean index = rule.getIndexOption() != Field.Index.NO;
451                 for (Object value : property) {
452                     if (value == null) continue;
453                     // Add a separate field for each property value ...
454                     int intValue = longFactory.create(value).intValue();
455                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setIntValue(intValue));
456                 }
457                 continue;
458             }
459             if (type == FieldType.DOUBLE) {
460                 ValueFactory<Double> doubleFactory = processor.valueFactories.getDoubleFactory();
461                 boolean index = rule.getIndexOption() != Field.Index.NO;
462                 for (Object value : property) {
463                     if (value == null) continue;
464                     // Add a separate field for each property value ...
465                     double dValue = doubleFactory.create(value);
466                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setDoubleValue(dValue));
467                 }
468                 continue;
469             }
470             if (type == FieldType.FLOAT) {
471                 ValueFactory<Double> doubleFactory = processor.valueFactories.getDoubleFactory();
472                 boolean index = rule.getIndexOption() != Field.Index.NO;
473                 for (Object value : property) {
474                     if (value == null) continue;
475                     // Add a separate field for each property value ...
476                     float fValue = doubleFactory.create(value).floatValue();
477                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setFloatValue(fValue));
478                 }
479                 continue;
480             }
481             if (type == FieldType.DECIMAL) {
482                 ValueFactory<BigDecimal> decimalFactory = processor.valueFactories.getDecimalFactory();
483                 for (Object value : property) {
484                     if (value == null) continue;
485                     BigDecimal decimal = decimalFactory.create(value);
486                     // Convert to a string that is lexicographically sortable ...
487                     value = FieldUtil.decimalToString(decimal);
488                     doc.add(new Field(nameString, stringValue, rule.getStoreOption(), Field.Index.NOT_ANALYZED));
489                 }
490                 continue;
491             }
492             if (type == FieldType.BINARY) {
493                 // TODO : add to full-text search ...
494                 continue;
495             }
496             if (type == FieldType.WEAK_REFERENCE) {
497                 ValueFactory<Path> pathFactory = processor.valueFactories.getPathFactory();
498                 for (Object value : property) {
499                     if (value == null) continue;
500                     // Add a separate field for each property value ...
501                     String valueStr = processor.stringFactory.create(pathFactory.create(value));
502                     doc.add(new Field(nameString, valueStr, rule.getStoreOption(), Field.Index.NOT_ANALYZED));
503                 }
504                 continue;
505             }
506             if (type == FieldType.REFERENCE) {
507                 for (Object value : property) {
508                     if (value == null) continue;
509                     // Obtain the string value of the reference (i.e., should be the string of the UUID) ...
510                     stringValue = processor.stringFactory.create(value);
511                     // Add a separate field for each property value in exact form (not analyzed) ...
512                     doc.add(new Field(nameString, stringValue, rule.getStoreOption(), Field.Index.NOT_ANALYZED));
513                     // Add a value to the common reference value ...
514                     doc.add(new Field(ContentIndex.REFERENCES, stringValue, Field.Store.NO, Field.Index.NOT_ANALYZED));
515                 }
516                 continue;
517             }
518             if (type == FieldType.BOOLEAN) {
519                 ValueFactory<Boolean> booleanFactory = processor.valueFactories.getBooleanFactory();
520                 boolean index = rule.getIndexOption() != Field.Index.NO;
521                 for (Object value : property) {
522                     if (value == null) continue;
523                     // Add a separate field for each property value ...
524                     int intValue = booleanFactory.create(value).booleanValue() ? 1 : 0;
525                     doc.add(new NumericField(nameString, rule.getStoreOption(), index).setIntValue(intValue));
526                 }
527                 continue;
528             }
529             assert type == FieldType.STRING;
530             for (Object value : property) {
531                 if (value == null) continue;
532                 stringValue = processor.stringFactory.create(value);
533                 // Add a separate field for each property value in exact form (not analyzed) ...
534                 doc.add(new Field(nameString, stringValue, rule.getStoreOption(), Field.Index.NOT_ANALYZED));
535 
536                 boolean treatedAsReference = false;
537                 if (rule.canBeReference()) {
538                     if (stringValue.length() == 36 && stringValue.charAt(8) == '-') {
539                         // The value looks like a string representation of a UUID ...
540                         try {
541                             UUID.fromString(stringValue);
542                             // Add a value to the common reference value ...
543                             treatedAsReference = true;
544                             doc.add(new Field(ContentIndex.REFERENCES, stringValue, Field.Store.YES, Field.Index.NOT_ANALYZED));
545                         } catch (IllegalArgumentException e) {
546                             // Must not conform to the UUID format
547                         }
548                     }
549                 }
550                 if (!treatedAsReference && rule.getIndexOption() != Field.Index.NO && rule.isFullTextSearchable()
551                     && !NON_SEARCHABLE_NAMES.contains(name)) {
552                     // This field is to be full-text searchable ...
553                     fullTextSearchValue.append(' ').append(stringValue);
554 
555                     // Also create a full-text-searchable field ...
556                     String fullTextNameString = processor.fullTextFieldName(nameString);
557                     doc.add(new Field(fullTextNameString, stringValue, Store.NO, Index.ANALYZED));
558                 }
559             }
560         }
561         // Add the full-text-search field ...
562         if (fullTextSearchValue.length() != 0) {
563             doc.add(new Field(ContentIndex.FULL_TEXT, fullTextSearchValue.toString(), Field.Store.NO, Field.Index.ANALYZED));
564         }
565         if (logger.isTraceEnabled()) {
566             logger.trace("index for \"{0}\" workspace: ADD {1} {2}", workspace.getWorkspaceName(), pathStr, doc);
567             if (fullTextSearchValue.length() != 0) {
568 
569                 // Run the expression through the Lucene analyzer to extract the terms ...
570                 String fullTextContent = fullTextSearchValue.toString();
571                 TokenStream stream = getAnalyzer().tokenStream(ContentIndex.FULL_TEXT, new StringReader(fullTextContent));
572                 TermAttribute term = stream.addAttribute(TermAttribute.class);
573                 // PositionIncrementAttribute positionIncrement = stream.addAttribute(PositionIncrementAttribute.class);
574                 // OffsetAttribute offset = stream.addAttribute(OffsetAttribute.class);
575                 // TypeAttribute type = stream.addAttribute(TypeAttribute.class);
576                 // int position = 0;
577                 StringBuilder output = new StringBuilder();
578                 while (stream.incrementToken()) {
579                     output.append(term.term()).append(' ');
580                     // // The term attribute object has been modified to contain the next term ...
581                     // int incr = positionIncrement.getPositionIncrement();
582                     // if (incr > 0) {
583                     // position = position + incr;
584                     // output.append(' ').append(position).append(':');
585                     // }
586                     // output.append('[')
587                     // .append(term.term())
588                     // .append(':')
589                     // .append(offset.startOffset())
590                     // .append("->")
591                     // .append(offset.endOffset())
592                     // .append(':')
593                     // .append(type.type())
594                     // .append(']');
595                 }
596                 logger.trace("index for \"{0}\" workspace:     {1} fts terms: {2}", workspace.getWorkspaceName(), pathStr, output);
597             }
598         }
599         getContentWriter().updateDocument(new Term(ContentIndex.PATH, pathStr), doc);
600     }
601 
602     /**
603      * {@inheritDoc}
604      * 
605      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#createTupleCollector(org.modeshape.graph.query.QueryResults.Columns)
606      */
607     public TupleCollector createTupleCollector( Columns columns ) {
608         return new DualIndexTupleCollector(this, columns);
609     }
610 
611     public Location getLocationForRoot() throws IOException {
612         // Look for the root node ...
613         Query query = NumericRangeQuery.newIntRange(ContentIndex.DEPTH, 0, 0, true, true);
614 
615         // Execute the search and place the results into the supplied list ...
616         List<Object[]> tuples = new ArrayList<Object[]>(1);
617         FullTextSearchTupleCollector collector = new FullTextSearchTupleCollector(this, tuples);
618         getContentSearcher().search(query, collector);
619 
620         // Extract the location from the results ...
621         return tuples.isEmpty() ? Location.create(processor.pathFactory.createRootPath()) : (Location)tuples.get(0)[0];
622     }
623 
624     public Query findAllNodesBelow( Path parentPath ) {
625         // Find the path of the parent ...
626         String stringifiedPath = processor.pathAsString(parentPath);
627         // Append a '/' to the parent path, and we'll only get decendants ...
628         stringifiedPath = stringifiedPath + '/';
629 
630         // Create a prefix query ...
631         return new PrefixQuery(new Term(ContentIndex.PATH, stringifiedPath));
632     }
633 
634     public Query findAllNodesAtOrBelow( Path parentPath ) {
635         if (parentPath.isRoot()) {
636             return new MatchAllDocsQuery();
637         }
638         // Find the path of the parent ...
639         String stringifiedPath = processor.pathAsString(parentPath);
640 
641         // Create a prefix query ...
642         return new PrefixQuery(new Term(ContentIndex.PATH, stringifiedPath));
643     }
644 
645     /**
646      * Return a query that can be used to find all of the documents that represent nodes that are children of the node at the
647      * supplied path.
648      * 
649      * @param parentPath the path of the parent node.
650      * @return the query; never null
651      */
652     public Query findChildNodes( Path parentPath ) {
653         // Find the path of the parent ...
654         String stringifiedPath = processor.pathAsString(parentPath);
655         // Append a '/' to the parent path, so we'll only get decendants ...
656         stringifiedPath = stringifiedPath + '/';
657 
658         // Create a query to find all the nodes below the parent path ...
659         Query query = new PrefixQuery(new Term(ContentIndex.PATH, stringifiedPath));
660         // Include only the children ...
661         int childrenDepth = parentPath.size() + 1;
662         Query depthQuery = NumericRangeQuery.newIntRange(ContentIndex.DEPTH, childrenDepth, childrenDepth, true, true);
663         // And combine ...
664         BooleanQuery combinedQuery = new BooleanQuery();
665         combinedQuery.add(query, Occur.MUST);
666         combinedQuery.add(depthQuery, Occur.MUST);
667         return combinedQuery;
668     }
669 
670     /**
671      * Create a query that can be used to find the one document (or node) that exists at the exact path supplied.
672      * 
673      * @param path the path of the node
674      * @return the query; never null
675      */
676     public Query findNodeAt( Path path ) {
677         if (path.isRoot()) {
678             // Look for the root node ...
679             return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, 0, 0, true, true);
680         }
681         String stringifiedPath = processor.pathAsString(path);
682         return new TermQuery(new Term(ContentIndex.PATH, stringifiedPath));
683     }
684 
685     public Query findNodesLike( String fieldName,
686                                 String likeExpression,
687                                 boolean caseSensitive ) {
688         ValueFactories factories = processor.valueFactories;
689         return CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression, fieldName, factories, caseSensitive);
690     }
691 
692     public Query findNodesWith( Length propertyLength,
693                                 Operator operator,
694                                 Object value ) {
695         assert propertyLength != null;
696         assert value != null;
697         PropertyValue propertyValue = propertyLength.propertyValue();
698         String field = processor.stringFactory.create(propertyValue.propertyName());
699         ValueFactories factories = processor.valueFactories;
700         int length = factories.getLongFactory().create(value).intValue();
701         switch (operator) {
702             case EQUAL_TO:
703                 return CompareLengthQuery.createQueryForNodesWithFieldEqualTo(length, field, factories);
704             case NOT_EQUAL_TO:
705                 return CompareLengthQuery.createQueryForNodesWithFieldNotEqualTo(length, field, factories);
706             case GREATER_THAN:
707                 return CompareLengthQuery.createQueryForNodesWithFieldGreaterThan(length, field, factories);
708             case GREATER_THAN_OR_EQUAL_TO:
709                 return CompareLengthQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(length, field, factories);
710             case LESS_THAN:
711                 return CompareLengthQuery.createQueryForNodesWithFieldLessThan(length, field, factories);
712             case LESS_THAN_OR_EQUAL_TO:
713                 return CompareLengthQuery.createQueryForNodesWithFieldLessThanOrEqualTo(length, field, factories);
714             case LIKE:
715                 // This is not allowed ...
716                 assert false;
717                 break;
718         }
719         return null;
720     }
721 
722     @SuppressWarnings( "unchecked" )
723     public Query findNodesWith( PropertyValue propertyValue,
724                                 Operator operator,
725                                 Object value,
726                                 boolean caseSensitive ) {
727         ValueFactory<String> stringFactory = processor.stringFactory;
728         String field = stringFactory.create(propertyValue.propertyName());
729         Name fieldName = processor.nameFactory.create(field);
730         ValueFactories factories = processor.valueFactories;
731         IndexRules.Rule rule = workspace.rules.getRule(fieldName);
732         if (rule == null || rule.isSkipped()) return new MatchNoneQuery();
733         FieldType type = rule.getType();
734         switch (type) {
735             case REFERENCE:
736             case WEAK_REFERENCE:
737             case DECIMAL: // stored in lexicographically-ordered form
738             case STRING:
739                 String stringValue = stringFactory.create(value);
740                 if (value instanceof Path) {
741                     stringValue = processor.pathAsString((Path)value);
742                 }
743                 if (!caseSensitive) stringValue = stringValue.toLowerCase();
744                 switch (operator) {
745                     case EQUAL_TO:
746                         return CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue,
747                                                                                       field,
748                                                                                       factories,
749                                                                                       caseSensitive);
750                     case NOT_EQUAL_TO:
751                         Query query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue,
752                                                                                              field,
753                                                                                              factories,
754                                                                                              caseSensitive);
755                         return new NotQuery(query);
756                     case GREATER_THAN:
757                         return CompareStringQuery.createQueryForNodesWithFieldGreaterThan(stringValue,
758                                                                                           field,
759                                                                                           factories,
760                                                                                           caseSensitive);
761                     case GREATER_THAN_OR_EQUAL_TO:
762                         return CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(stringValue,
763                                                                                                    field,
764                                                                                                    factories,
765                                                                                                    caseSensitive);
766                     case LESS_THAN:
767                         return CompareStringQuery.createQueryForNodesWithFieldLessThan(stringValue,
768                                                                                        field,
769                                                                                        factories,
770                                                                                        caseSensitive);
771                     case LESS_THAN_OR_EQUAL_TO:
772                         return CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(stringValue,
773                                                                                                 field,
774                                                                                                 factories,
775                                                                                                 caseSensitive);
776                     case LIKE:
777                         return findNodesLike(field, stringValue, caseSensitive);
778                 }
779                 break;
780             case DATE:
781                 NumericRule<Long> longRule = (NumericRule<Long>)rule;
782                 long date = factories.getLongFactory().create(value);
783                 switch (operator) {
784                     case EQUAL_TO:
785                         if (date < longRule.getMinimum() || date > longRule.getMaximum()) return new MatchNoneQuery();
786                         return NumericRangeQuery.newLongRange(field, date, date, true, true);
787                     case NOT_EQUAL_TO:
788                         if (date < longRule.getMinimum() || date > longRule.getMaximum()) return new MatchAllDocsQuery();
789                         Query query = NumericRangeQuery.newLongRange(field, date, date, true, true);
790                         return new NotQuery(query);
791                     case GREATER_THAN:
792                         if (date > longRule.getMaximum()) return new MatchNoneQuery();
793                         return NumericRangeQuery.newLongRange(field, date, longRule.getMaximum(), false, true);
794                     case GREATER_THAN_OR_EQUAL_TO:
795                         if (date > longRule.getMaximum()) return new MatchNoneQuery();
796                         return NumericRangeQuery.newLongRange(field, date, longRule.getMaximum(), true, true);
797                     case LESS_THAN:
798                         if (date < longRule.getMinimum()) return new MatchNoneQuery();
799                         return NumericRangeQuery.newLongRange(field, longRule.getMinimum(), date, true, false);
800                     case LESS_THAN_OR_EQUAL_TO:
801                         if (date < longRule.getMinimum()) return new MatchNoneQuery();
802                         return NumericRangeQuery.newLongRange(field, longRule.getMinimum(), date, true, true);
803                     case LIKE:
804                         // This is not allowed ...
805                         assert false;
806                         return null;
807                 }
808                 break;
809             case LONG:
810                 longRule = (NumericRule<Long>)rule;
811                 long longValue = factories.getLongFactory().create(value);
812                 switch (operator) {
813                     case EQUAL_TO:
814                         if (longValue < longRule.getMinimum() || longValue > longRule.getMaximum()) return new MatchNoneQuery();
815                         return NumericRangeQuery.newLongRange(field, longValue, longValue, true, true);
816                     case NOT_EQUAL_TO:
817                         if (longValue < longRule.getMinimum() || longValue > longRule.getMaximum()) return new MatchAllDocsQuery();
818                         Query query = NumericRangeQuery.newLongRange(field, longValue, longValue, true, true);
819                         return new NotQuery(query);
820                     case GREATER_THAN:
821                         if (longValue > longRule.getMaximum()) return new MatchNoneQuery();
822                         return NumericRangeQuery.newLongRange(field, longValue, longRule.getMaximum(), false, true);
823                     case GREATER_THAN_OR_EQUAL_TO:
824                         if (longValue > longRule.getMaximum()) return new MatchNoneQuery();
825                         return NumericRangeQuery.newLongRange(field, longValue, longRule.getMaximum(), true, true);
826                     case LESS_THAN:
827                         if (longValue < longRule.getMinimum()) return new MatchNoneQuery();
828                         return NumericRangeQuery.newLongRange(field, longRule.getMinimum(), longValue, true, false);
829                     case LESS_THAN_OR_EQUAL_TO:
830                         if (longValue < longRule.getMinimum()) return new MatchNoneQuery();
831                         return NumericRangeQuery.newLongRange(field, longRule.getMinimum(), longValue, true, true);
832                     case LIKE:
833                         // This is not allowed ...
834                         assert false;
835                         return null;
836                 }
837                 break;
838             case BOOLEAN:
839                 boolean booleanValue = factories.getBooleanFactory().create(value);
840                 stringValue = stringFactory.create(value);
841                 switch (operator) {
842                     case EQUAL_TO:
843                         return new TermQuery(new Term(field, stringValue));
844                     case NOT_EQUAL_TO:
845                         return new TermQuery(new Term(field, stringFactory.create(!booleanValue)));
846                     case GREATER_THAN:
847                         if (!booleanValue) {
848                             return new TermQuery(new Term(field, stringFactory.create(true)));
849                         }
850                         // Can't be greater than 'true', per JCR spec
851                         return new MatchNoneQuery();
852                     case GREATER_THAN_OR_EQUAL_TO:
853                         return new TermQuery(new Term(field, stringFactory.create(true)));
854                     case LESS_THAN:
855                         if (booleanValue) {
856                             return new TermQuery(new Term(field, stringFactory.create(false)));
857                         }
858                         // Can't be less than 'false', per JCR spec
859                         return new MatchNoneQuery();
860                     case LESS_THAN_OR_EQUAL_TO:
861                         return new TermQuery(new Term(field, stringFactory.create(false)));
862                     case LIKE:
863                         // This is not allowed ...
864                         assert false;
865                         return null;
866                 }
867                 break;
868             case INT:
869                 NumericRule<Integer> intRule = (NumericRule<Integer>)rule;
870                 int intValue = factories.getLongFactory().create(value).intValue();
871                 switch (operator) {
872                     case EQUAL_TO:
873                         if (intValue < intRule.getMinimum() || intValue > intRule.getMaximum()) return new MatchNoneQuery();
874                         return NumericRangeQuery.newIntRange(field, intValue, intValue, true, true);
875                     case NOT_EQUAL_TO:
876                         if (intValue < intRule.getMinimum() || intValue > intRule.getMaximum()) return new MatchAllDocsQuery();
877                         Query query = NumericRangeQuery.newIntRange(field, intValue, intValue, true, true);
878                         return new NotQuery(query);
879                     case GREATER_THAN:
880                         if (intValue > intRule.getMaximum()) return new MatchNoneQuery();
881                         return NumericRangeQuery.newIntRange(field, intValue, intRule.getMaximum(), false, true);
882                     case GREATER_THAN_OR_EQUAL_TO:
883                         if (intValue > intRule.getMaximum()) return new MatchNoneQuery();
884                         return NumericRangeQuery.newIntRange(field, intValue, intRule.getMaximum(), true, true);
885                     case LESS_THAN:
886                         if (intValue < intRule.getMinimum()) return new MatchNoneQuery();
887                         return NumericRangeQuery.newIntRange(field, intRule.getMinimum(), intValue, true, false);
888                     case LESS_THAN_OR_EQUAL_TO:
889                         if (intValue < intRule.getMinimum()) return new MatchNoneQuery();
890                         return NumericRangeQuery.newIntRange(field, intRule.getMinimum(), intValue, true, true);
891                     case LIKE:
892                         // This is not allowed ...
893                         assert false;
894                         return null;
895                 }
896                 break;
897             case DOUBLE:
898                 NumericRule<Double> dRule = (NumericRule<Double>)rule;
899                 double doubleValue = factories.getDoubleFactory().create(value);
900                 switch (operator) {
901                     case EQUAL_TO:
902                         if (doubleValue < dRule.getMinimum() || doubleValue > dRule.getMaximum()) return new MatchNoneQuery();
903                         return NumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true);
904                     case NOT_EQUAL_TO:
905                         if (doubleValue < dRule.getMinimum() || doubleValue > dRule.getMaximum()) return new MatchAllDocsQuery();
906                         Query query = NumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true);
907                         return new NotQuery(query);
908                     case GREATER_THAN:
909                         if (doubleValue > dRule.getMaximum()) return new MatchNoneQuery();
910                         return NumericRangeQuery.newDoubleRange(field, doubleValue, dRule.getMaximum(), false, true);
911                     case GREATER_THAN_OR_EQUAL_TO:
912                         if (doubleValue > dRule.getMaximum()) return new MatchNoneQuery();
913                         return NumericRangeQuery.newDoubleRange(field, doubleValue, dRule.getMaximum(), true, true);
914                     case LESS_THAN:
915                         if (doubleValue < dRule.getMinimum()) return new MatchNoneQuery();
916                         return NumericRangeQuery.newDoubleRange(field, dRule.getMinimum(), doubleValue, true, false);
917                     case LESS_THAN_OR_EQUAL_TO:
918                         if (doubleValue < dRule.getMinimum()) return new MatchNoneQuery();
919                         return NumericRangeQuery.newDoubleRange(field, dRule.getMinimum(), doubleValue, true, true);
920                     case LIKE:
921                         // This is not allowed ...
922                         assert false;
923                         return null;
924                 }
925                 break;
926             case FLOAT:
927                 NumericRule<Float> fRule = (NumericRule<Float>)rule;
928                 float floatValue = factories.getDoubleFactory().create(value).floatValue();
929                 switch (operator) {
930                     case EQUAL_TO:
931                         if (floatValue < fRule.getMinimum() || floatValue > fRule.getMaximum()) return new MatchNoneQuery();
932                         return NumericRangeQuery.newFloatRange(field, floatValue, floatValue, true, true);
933                     case NOT_EQUAL_TO:
934                         if (floatValue < fRule.getMinimum() || floatValue > fRule.getMaximum()) return new MatchAllDocsQuery();
935                         Query query = NumericRangeQuery.newFloatRange(field, floatValue, floatValue, true, true);
936                         return new NotQuery(query);
937                     case GREATER_THAN:
938                         if (floatValue > fRule.getMaximum()) return new MatchNoneQuery();
939                         return NumericRangeQuery.newFloatRange(field, floatValue, fRule.getMaximum(), false, true);
940                     case GREATER_THAN_OR_EQUAL_TO:
941                         if (floatValue > fRule.getMaximum()) return new MatchNoneQuery();
942                         return NumericRangeQuery.newFloatRange(field, floatValue, fRule.getMaximum(), true, true);
943                     case LESS_THAN:
944                         if (floatValue < fRule.getMinimum()) return new MatchNoneQuery();
945                         return NumericRangeQuery.newFloatRange(field, fRule.getMinimum(), floatValue, true, false);
946                     case LESS_THAN_OR_EQUAL_TO:
947                         if (floatValue < fRule.getMinimum()) return new MatchNoneQuery();
948                         return NumericRangeQuery.newFloatRange(field, fRule.getMinimum(), floatValue, true, true);
949                     case LIKE:
950                         // This is not allowed ...
951                         assert false;
952                         return null;
953                 }
954                 break;
955             case BINARY:
956                 // This is not allowed ...
957                 assert false;
958                 return null;
959         }
960         return null;
961     }
962 
963     /**
964      * {@inheritDoc}
965      * 
966      * @see org.modeshape.search.lucene.AbstractLuceneSearchEngine.WorkspaceSession#findNodesWith(org.modeshape.graph.query.model.ReferenceValue,
967      *      org.modeshape.graph.query.model.Operator, java.lang.Object)
968      */
969     @Override
970     public Query findNodesWith( ReferenceValue referenceValue,
971                                 Operator operator,
972                                 Object value ) {
973         String field = referenceValue.propertyName();
974         if (field == null) field = LuceneSearchWorkspace.ContentIndex.REFERENCES;
975         ValueFactories factories = processor.valueFactories;
976         String stringValue = processor.stringFactory.create(value);
977         switch (operator) {
978             case EQUAL_TO:
979                 return CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, true);
980             case NOT_EQUAL_TO:
981                 return new NotQuery(CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, true));
982             case GREATER_THAN:
983                 return CompareStringQuery.createQueryForNodesWithFieldGreaterThan(stringValue, field, factories, true);
984             case GREATER_THAN_OR_EQUAL_TO:
985                 return CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(stringValue, field, factories, true);
986             case LESS_THAN:
987                 return CompareStringQuery.createQueryForNodesWithFieldLessThan(stringValue, field, factories, true);
988             case LESS_THAN_OR_EQUAL_TO:
989                 return CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(stringValue, field, factories, true);
990             case LIKE:
991                 return findNodesLike(field, stringValue, false);
992         }
993         return null;
994     }
995 
996     public Query findNodesWithNumericRange( PropertyValue propertyValue,
997                                             Object lowerValue,
998                                             Object upperValue,
999                                             boolean includesLower,
1000                                             boolean includesUpper ) {
1001         String field = processor.stringFactory.create(propertyValue.propertyName());
1002         return findNodesWithNumericRange(field, lowerValue, upperValue, includesLower, includesUpper);
1003     }
1004 
1005     public Query findNodesWithNumericRange( NodeDepth depth,
1006                                             Object lowerValue,
1007                                             Object upperValue,
1008                                             boolean includesLower,
1009                                             boolean includesUpper ) {
1010         return findNodesWithNumericRange(ContentIndex.DEPTH, lowerValue, upperValue, includesLower, includesUpper);
1011     }
1012 
1013     protected Query findNodesWithNumericRange( String field,
1014                                                Object lowerValue,
1015                                                Object upperValue,
1016                                                boolean includesLower,
1017                                                boolean includesUpper ) {
1018         Name fieldName = processor.nameFactory.create(field);
1019         IndexRules.Rule rule = workspace.rules.getRule(fieldName);
1020         if (rule == null || rule.isSkipped()) return new MatchNoneQuery();
1021         FieldType type = rule.getType();
1022         ValueFactories factories = processor.valueFactories;
1023         switch (type) {
1024             case DATE:
1025                 long lowerDate = factories.getLongFactory().create(lowerValue);
1026                 long upperDate = factories.getLongFactory().create(upperValue);
1027                 return NumericRangeQuery.newLongRange(field, lowerDate, upperDate, includesLower, includesUpper);
1028             case LONG:
1029                 long lowerLong = factories.getLongFactory().create(lowerValue);
1030                 long upperLong = factories.getLongFactory().create(upperValue);
1031                 return NumericRangeQuery.newLongRange(field, lowerLong, upperLong, includesLower, includesUpper);
1032             case DOUBLE:
1033                 double lowerDouble = factories.getDoubleFactory().create(lowerValue);
1034                 double upperDouble = factories.getDoubleFactory().create(upperValue);
1035                 return NumericRangeQuery.newDoubleRange(field, lowerDouble, upperDouble, includesLower, includesUpper);
1036             case FLOAT:
1037                 float lowerFloat = factories.getDoubleFactory().create(lowerValue).floatValue();
1038                 float upperFloat = factories.getDoubleFactory().create(upperValue).floatValue();
1039                 return NumericRangeQuery.newFloatRange(field, lowerFloat, upperFloat, includesLower, includesUpper);
1040             case INT:
1041                 int lowerInt = factories.getLongFactory().create(lowerValue).intValue();
1042                 int upperInt = factories.getLongFactory().create(upperValue).intValue();
1043                 return NumericRangeQuery.newIntRange(field, lowerInt, upperInt, includesLower, includesUpper);
1044             case BOOLEAN:
1045                 lowerInt = factories.getBooleanFactory().create(lowerValue).booleanValue() ? 1 : 0;
1046                 upperInt = factories.getBooleanFactory().create(upperValue).booleanValue() ? 1 : 0;
1047                 return NumericRangeQuery.newIntRange(field, lowerInt, upperInt, includesLower, includesUpper);
1048             case DECIMAL:
1049                 BigDecimal lowerDecimal = factories.getDecimalFactory().create(lowerValue);
1050                 BigDecimal upperDecimal = factories.getDecimalFactory().create(upperValue);
1051                 String lsv = FieldUtil.decimalToString(lowerDecimal);
1052                 String usv = FieldUtil.decimalToString(upperDecimal);
1053                 Query lower = null;
1054                 if (includesLower) {
1055                     lower = CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(lsv, field, factories, false);
1056                 } else {
1057                     lower = CompareStringQuery.createQueryForNodesWithFieldGreaterThan(lsv, field, factories, false);
1058                 }
1059                 Query upper = null;
1060                 if (includesUpper) {
1061                     upper = CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(usv, field, factories, false);
1062                 } else {
1063                     upper = CompareStringQuery.createQueryForNodesWithFieldLessThan(usv, field, factories, false);
1064                 }
1065                 BooleanQuery query = new BooleanQuery();
1066                 query.add(lower, Occur.MUST);
1067                 query.add(upper, Occur.MUST);
1068                 return query;
1069             case STRING:
1070             case REFERENCE:
1071             case WEAK_REFERENCE:
1072             case BINARY:
1073                 assert false;
1074         }
1075         return new MatchNoneQuery();
1076     }
1077 
1078     protected String likeExpresionForWildcardPath( String path ) {
1079         if (path.equals("/") || path.equals("%")) return path;
1080         StringBuilder sb = new StringBuilder();
1081         path = path.replaceAll("%+", "%");
1082         if (path.startsWith("%/")) {
1083             sb.append("%");
1084             if (path.length() == 2) return sb.toString();
1085             path = path.substring(2);
1086         }
1087         for (String segment : path.split("/")) {
1088             if (segment.length() == 0) continue;
1089             sb.append("/");
1090             sb.append(segment);
1091             if (segment.equals("%") || segment.equals("_")) continue;
1092             if (!segment.endsWith("]") && !segment.endsWith("]%") && !segment.endsWith("]_")) {
1093                 sb.append("[1]");
1094             }
1095         }
1096         if (path.endsWith("/")) sb.append("/");
1097         return sb.toString();
1098     }
1099 
1100     public Query findNodesWith( NodePath nodePath,
1101                                 Operator operator,
1102                                 Object value,
1103                                 boolean caseSensitive ) {
1104         if (!caseSensitive) value = processor.stringFactory.create(value).toLowerCase();
1105         Path pathValue = operator != Operator.LIKE ? processor.pathFactory.create(value) : null;
1106         Query query = null;
1107         switch (operator) {
1108             case EQUAL_TO:
1109                 return findNodeAt(pathValue);
1110             case NOT_EQUAL_TO:
1111                 return new NotQuery(findNodeAt(pathValue));
1112             case LIKE:
1113                 String likeExpression = processor.stringFactory.create(value);
1114                 likeExpression = likeExpresionForWildcardPath(likeExpression);
1115                 if (likeExpression.indexOf("[%]") != -1) {
1116                     // We can't use '[%]' because we only want to match digits,
1117                     // so handle this using a regex ...
1118                     String regex = likeExpression;
1119                     regex = regex.replace("[%]", "[\\d+]");
1120                     regex = regex.replace("[", "\\[");
1121                     regex = regex.replace("*", ".*").replace("?", ".");
1122                     regex = regex.replace("%", ".*").replace("_", ".");
1123                     // Now create a regex query ...
1124                     RegexQuery regexQuery = new RegexQuery(new Term(ContentIndex.PATH, regex));
1125                     int flags = caseSensitive ? 0 : Pattern.CASE_INSENSITIVE;
1126                     regexQuery.setRegexImplementation(new JavaUtilRegexCapabilities(flags));
1127                     query = regexQuery;
1128                 } else {
1129                     query = findNodesLike(ContentIndex.PATH, likeExpression, caseSensitive);
1130                 }
1131                 break;
1132             case GREATER_THAN:
1133                 query = ComparePathQuery.createQueryForNodesWithPathGreaterThan(pathValue,
1134                                                                                 ContentIndex.PATH,
1135                                                                                 processor.valueFactories,
1136                                                                                 caseSensitive);
1137                 break;
1138             case GREATER_THAN_OR_EQUAL_TO:
1139                 query = ComparePathQuery.createQueryForNodesWithPathGreaterThanOrEqualTo(pathValue,
1140                                                                                          ContentIndex.PATH,
1141                                                                                          processor.valueFactories,
1142                                                                                          caseSensitive);
1143                 break;
1144             case LESS_THAN:
1145                 query = ComparePathQuery.createQueryForNodesWithPathLessThan(pathValue,
1146                                                                              ContentIndex.PATH,
1147                                                                              processor.valueFactories,
1148                                                                              caseSensitive);
1149                 break;
1150             case LESS_THAN_OR_EQUAL_TO:
1151                 query = ComparePathQuery.createQueryForNodesWithPathLessThanOrEqualTo(pathValue,
1152                                                                                       ContentIndex.PATH,
1153                                                                                       processor.valueFactories,
1154                                                                                       caseSensitive);
1155                 break;
1156         }
1157         return query;
1158     }
1159 
1160     public Query findNodesWith( NodeName nodeName,
1161                                 Operator operator,
1162                                 Object value,
1163                                 boolean caseSensitive ) {
1164         ValueFactories factories = processor.valueFactories;
1165         String stringValue = processor.stringFactory.create(value);
1166         if (!caseSensitive) stringValue = stringValue.toLowerCase();
1167         Path.Segment segment = operator != Operator.LIKE ? processor.pathFactory.createSegment(stringValue) : null;
1168         // Determine if the string value contained a SNS index ...
1169         boolean includeSns = stringValue.indexOf('[') != -1;
1170         int snsIndex = operator != Operator.LIKE ? segment.getIndex() : 0;
1171         Query query = null;
1172         switch (operator) {
1173             case EQUAL_TO:
1174                 if (!includeSns) {
1175                     return new TermQuery(new Term(ContentIndex.NODE_NAME, stringValue));
1176                 }
1177                 BooleanQuery booleanQuery = new BooleanQuery();
1178                 booleanQuery.add(new TermQuery(new Term(ContentIndex.NODE_NAME, stringValue)), Occur.MUST);
1179                 booleanQuery.add(NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, snsIndex, snsIndex, true, true),
1180                                  Occur.MUST);
1181                 return booleanQuery;
1182             case NOT_EQUAL_TO:
1183                 if (!includeSns) {
1184                     return new NotQuery(new TermQuery(new Term(ContentIndex.NODE_NAME, stringValue)));
1185                 }
1186                 booleanQuery = new BooleanQuery();
1187                 booleanQuery.add(new TermQuery(new Term(ContentIndex.NODE_NAME, stringValue)), Occur.MUST);
1188                 booleanQuery.add(NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, snsIndex, snsIndex, true, true),
1189                                  Occur.MUST);
1190                 return new NotQuery(booleanQuery);
1191             case GREATER_THAN:
1192                 query = CompareNameQuery.createQueryForNodesWithNameGreaterThan(segment,
1193                                                                                 ContentIndex.NODE_NAME,
1194                                                                                 ContentIndex.SNS_INDEX,
1195                                                                                 factories,
1196                                                                                 caseSensitive,
1197                                                                                 includeSns);
1198                 break;
1199             case GREATER_THAN_OR_EQUAL_TO:
1200                 query = CompareNameQuery.createQueryForNodesWithNameGreaterThanOrEqualTo(segment,
1201                                                                                          ContentIndex.NODE_NAME,
1202                                                                                          ContentIndex.SNS_INDEX,
1203                                                                                          factories,
1204                                                                                          caseSensitive,
1205                                                                                          includeSns);
1206                 break;
1207             case LESS_THAN:
1208                 query = CompareNameQuery.createQueryForNodesWithNameLessThan(segment,
1209                                                                              ContentIndex.NODE_NAME,
1210                                                                              ContentIndex.SNS_INDEX,
1211                                                                              factories,
1212                                                                              caseSensitive,
1213                                                                              includeSns);
1214                 break;
1215             case LESS_THAN_OR_EQUAL_TO:
1216                 query = CompareNameQuery.createQueryForNodesWithNameLessThanOrEqualTo(segment,
1217                                                                                       ContentIndex.NODE_NAME,
1218                                                                                       ContentIndex.SNS_INDEX,
1219                                                                                       factories,
1220                                                                                       caseSensitive,
1221                                                                                       includeSns);
1222                 break;
1223             case LIKE:
1224                 // See whether the like expression has brackets ...
1225                 String likeExpression = stringValue;
1226                 int openBracketIndex = likeExpression.indexOf('[');
1227                 if (openBracketIndex != -1) {
1228                     String localNameExpression = likeExpression.substring(0, openBracketIndex);
1229                     String snsIndexExpression = likeExpression.substring(openBracketIndex);
1230                     Query localNameQuery = CompareStringQuery.createQueryForNodesWithFieldLike(localNameExpression,
1231                                                                                                ContentIndex.NODE_NAME,
1232                                                                                                factories,
1233                                                                                                caseSensitive);
1234                     Query snsQuery = createSnsIndexQuery(snsIndexExpression);
1235                     if (localNameQuery == null) {
1236                         if (snsQuery == null) {
1237                             query = new MatchNoneQuery();
1238                         } else {
1239                             // There is just an SNS part ...
1240                             query = snsQuery;
1241                         }
1242                     } else {
1243                         // There is a local name part ...
1244                         if (snsQuery == null) {
1245                             query = localNameQuery;
1246                         } else {
1247                             // There is both a local name part and a SNS part ...
1248                             booleanQuery = new BooleanQuery();
1249                             booleanQuery.add(localNameQuery, Occur.MUST);
1250                             booleanQuery.add(snsQuery, Occur.MUST);
1251                             query = booleanQuery;
1252                         }
1253                     }
1254                 } else {
1255                     // There is no SNS expression ...
1256                     query = CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression,
1257                                                                                 ContentIndex.NODE_NAME,
1258                                                                                 factories,
1259                                                                                 caseSensitive);
1260                 }
1261                 assert query != null;
1262                 break;
1263         }
1264         return query;
1265     }
1266 
1267     public Query findNodesWith( NodeLocalName nodeName,
1268                                 Operator operator,
1269                                 Object value,
1270                                 boolean caseSensitive ) {
1271         String nameValue = processor.stringFactory.create(value);
1272         Query query = null;
1273         switch (operator) {
1274             case LIKE:
1275                 String likeExpression = processor.stringFactory.create(value);
1276                 query = findNodesLike(ContentIndex.LOCAL_NAME, likeExpression, caseSensitive);
1277                 break;
1278             case EQUAL_TO:
1279                 query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(nameValue,
1280                                                                                ContentIndex.LOCAL_NAME,
1281                                                                                processor.valueFactories,
1282                                                                                caseSensitive);
1283                 break;
1284             case NOT_EQUAL_TO:
1285                 query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(nameValue,
1286                                                                                ContentIndex.LOCAL_NAME,
1287                                                                                processor.valueFactories,
1288                                                                                caseSensitive);
1289                 query = new NotQuery(query);
1290                 break;
1291             case GREATER_THAN:
1292                 query = CompareStringQuery.createQueryForNodesWithFieldGreaterThan(nameValue,
1293                                                                                    ContentIndex.LOCAL_NAME,
1294                                                                                    processor.valueFactories,
1295                                                                                    caseSensitive);
1296                 break;
1297             case GREATER_THAN_OR_EQUAL_TO:
1298                 query = CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(nameValue,
1299                                                                                             ContentIndex.LOCAL_NAME,
1300                                                                                             processor.valueFactories,
1301                                                                                             caseSensitive);
1302                 break;
1303             case LESS_THAN:
1304                 query = CompareStringQuery.createQueryForNodesWithFieldLessThan(nameValue,
1305                                                                                 ContentIndex.LOCAL_NAME,
1306                                                                                 processor.valueFactories,
1307                                                                                 caseSensitive);
1308                 break;
1309             case LESS_THAN_OR_EQUAL_TO:
1310                 query = CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(nameValue,
1311                                                                                          ContentIndex.LOCAL_NAME,
1312                                                                                          processor.valueFactories,
1313                                                                                          caseSensitive);
1314                 break;
1315         }
1316         return query;
1317     }
1318 
1319     public Query findNodesWith( NodeDepth depthConstraint,
1320                                 Operator operator,
1321                                 Object value ) {
1322         int depth = processor.valueFactories.getLongFactory().create(value).intValue();
1323         switch (operator) {
1324             case EQUAL_TO:
1325                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, depth, depth, true, true);
1326             case NOT_EQUAL_TO:
1327                 Query query = NumericRangeQuery.newIntRange(ContentIndex.DEPTH, depth, depth, true, true);
1328                 return new NotQuery(query);
1329             case GREATER_THAN:
1330                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, depth, MAX_DEPTH, false, true);
1331             case GREATER_THAN_OR_EQUAL_TO:
1332                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, depth, MAX_DEPTH, true, true);
1333             case LESS_THAN:
1334                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, MIN_DEPTH, depth, true, false);
1335             case LESS_THAN_OR_EQUAL_TO:
1336                 return NumericRangeQuery.newIntRange(ContentIndex.DEPTH, MIN_DEPTH, depth, true, true);
1337             case LIKE:
1338                 // This is not allowed ...
1339                 return null;
1340         }
1341         return null;
1342     }
1343 
1344     protected Query createLocalNameQuery( String likeExpression,
1345                                           boolean caseSensitive ) {
1346         if (likeExpression == null) return null;
1347         return CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression,
1348                                                                    ContentIndex.LOCAL_NAME,
1349                                                                    processor.valueFactories,
1350                                                                    caseSensitive);
1351     }
1352 
1353     /**
1354      * Utility method to generate a query against the SNS indexes. This method attempts to generate a query that works most
1355      * efficiently, depending upon the supplied expression. For example, if the supplied expression is just "[3]", then a range
1356      * query is used to find all values matching '3'. However, if "[3_]" is used (where '_' matches any single-character, or digit
1357      * in this case), then a range query is used to find all values between '30' and '39'. Similarly, if "[3%]" is used, then a
1358      * regular expression query is used.
1359      * 
1360      * @param likeExpression the expression that uses the JCR 2.0 LIKE representation, and which includes the leading '[' and
1361      *        trailing ']' characters
1362      * @return the query, or null if the expression cannot be represented as a query
1363      */
1364     protected Query createSnsIndexQuery( String likeExpression ) {
1365         if (likeExpression == null) return null;
1366         likeExpression = likeExpression.trim();
1367         if (likeExpression.length() == 0) return null;
1368 
1369         // Remove the leading '[' ...
1370         assert likeExpression.charAt(0) == '[';
1371         likeExpression = likeExpression.substring(1);
1372 
1373         // Remove the trailing ']' if it exists ...
1374         int closeBracketIndex = likeExpression.indexOf(']');
1375         if (closeBracketIndex != -1) {
1376             likeExpression = likeExpression.substring(0, closeBracketIndex);
1377         }
1378         if (likeExpression.equals("_")) {
1379             // The SNS expression can only be one digit ...
1380             return NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, MIN_SNS_INDEX, 9, true, true);
1381         }
1382         if (likeExpression.equals("%")) {
1383             // The SNS expression can be any digits ...
1384             return NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, MIN_SNS_INDEX, MAX_SNS_INDEX, true, true);
1385         }
1386         if (likeExpression.indexOf('_') != -1) {
1387             if (likeExpression.indexOf('%') != -1) {
1388                 // Contains both ...
1389                 return findNodesLike(ContentIndex.SNS_INDEX, likeExpression, true);
1390             }
1391             // It presumably contains some numbers and at least one '_' character ...
1392             int firstWildcardChar = likeExpression.indexOf('_');
1393             if (firstWildcardChar + 1 < likeExpression.length()) {
1394                 // There's at least some characters after the first '_' ...
1395                 int secondWildcardChar = likeExpression.indexOf('_', firstWildcardChar + 1);
1396                 if (secondWildcardChar != -1) {
1397                     // There are multiple '_' characters ...
1398                     return findNodesLike(ContentIndex.SNS_INDEX, likeExpression, true);
1399                 }
1400             }
1401             // There's only one '_', so parse the lowermost value and uppermost value ...
1402             String lowerExpression = likeExpression.replace('_', '0');
1403             String upperExpression = likeExpression.replace('_', '9');
1404             try {
1405                 // This SNS is just a number ...
1406                 int lowerSns = Integer.parseInt(lowerExpression);
1407                 int upperSns = Integer.parseInt(upperExpression);
1408                 return NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, lowerSns, upperSns, true, true);
1409             } catch (NumberFormatException e) {
1410                 // It's not a number but it's in the SNS field, so there will be no results ...
1411                 return new MatchNoneQuery();
1412             }
1413         }
1414         if (likeExpression.indexOf('%') != -1) {
1415             // It presumably contains some numbers and at least one '%' character ...
1416             return findNodesLike(ContentIndex.SNS_INDEX, likeExpression, true);
1417         }
1418         // This is not a LIKE expression but an exact value specification and should be a number ...
1419         try {
1420             // This SNS is just a number ...
1421             int sns = Integer.parseInt(likeExpression);
1422             return NumericRangeQuery.newIntRange(ContentIndex.SNS_INDEX, sns, sns, true, true);
1423         } catch (NumberFormatException e) {
1424             // It's not a number but it's in the SNS field, so there will be no results ...
1425             return new MatchNoneQuery();
1426         }
1427     }
1428 
1429     /**
1430      * This collector is responsible for loading the value for each of the columns into each tuple array.
1431      */
1432     protected static class DualIndexTupleCollector extends TupleCollector {
1433         private final LuceneSearchSession session;
1434         private final LinkedList<Object[]> tuples = new LinkedList<Object[]>();
1435         private final Columns columns;
1436         private final int numValues;
1437         private final boolean recordScore;
1438         private final int scoreIndex;
1439         private final FieldSelector fieldSelector;
1440         private final int locationIndex;
1441         private Scorer scorer;
1442         private IndexReader currentReader;
1443         private int docOffset;
1444 
1445         protected DualIndexTupleCollector( LuceneSearchSession session,
1446                                            Columns columns ) {
1447             this.session = session;
1448             this.columns = columns;
1449             assert this.columns != null;
1450             this.numValues = this.columns.getTupleSize();
1451             assert this.numValues >= 0;
1452             assert this.columns.getSelectorNames().size() == 1;
1453             final String selectorName = this.columns.getSelectorNames().get(0);
1454             this.locationIndex = this.columns.getLocationIndex(selectorName);
1455             this.recordScore = this.columns.hasFullTextSearchScores();
1456             this.scoreIndex = this.recordScore ? this.columns.getFullTextSearchScoreIndexFor(selectorName) : -1;
1457 
1458             // Create the set of field names that we need to load from the document ...
1459             final Set<String> fieldNames = new HashSet<String>(this.columns.getColumnNames());
1460             fieldNames.add(ContentIndex.LOCATION_ID_PROPERTIES); // add the UUID, which we'll put into the Location ...
1461             fieldNames.add(ContentIndex.PATH); // add the UUID, which we'll put into the Location ...
1462             this.fieldSelector = new FieldSelector() {
1463                 private static final long serialVersionUID = 1L;
1464 
1465                 public FieldSelectorResult accept( String fieldName ) {
1466                     return fieldNames.contains(fieldName) ? FieldSelectorResult.LOAD : FieldSelectorResult.NO_LOAD;
1467                 }
1468             };
1469         }
1470 
1471         /**
1472          * @return tuples
1473          */
1474         @Override
1475         public LinkedList<Object[]> getTuples() {
1476             return tuples;
1477         }
1478 
1479         /**
1480          * {@inheritDoc}
1481          * 
1482          * @see org.apache.lucene.search.Collector#acceptsDocsOutOfOrder()
1483          */
1484         @Override
1485         public boolean acceptsDocsOutOfOrder() {
1486             return true;
1487         }
1488 
1489         /**
1490          * Get the location index.
1491          * 
1492          * @return locationIndex
1493          */
1494         public int getLocationIndex() {
1495             return locationIndex;
1496         }
1497 
1498         /**
1499          * {@inheritDoc}
1500          * 
1501          * @see org.apache.lucene.search.Collector#setNextReader(org.apache.lucene.index.IndexReader, int)
1502          */
1503         @Override
1504         public void setNextReader( IndexReader reader,
1505                                    int docBase ) {
1506             this.currentReader = reader;
1507             this.docOffset = docBase;
1508         }
1509 
1510         /**
1511          * {@inheritDoc}
1512          * 
1513          * @see org.apache.lucene.search.Collector#setScorer(org.apache.lucene.search.Scorer)
1514          */
1515         @Override
1516         public void setScorer( Scorer scorer ) {
1517             this.scorer = scorer;
1518         }
1519 
1520         /**
1521          * {@inheritDoc}
1522          * 
1523          * @see org.apache.lucene.search.Collector#collect(int)
1524          */
1525         @Override
1526         public void collect( int doc ) throws IOException {
1527             int docId = doc + docOffset;
1528             Object[] tuple = new Object[numValues];
1529             Document document = currentReader.document(docId, fieldSelector);
1530             for (String columnName : columns.getColumnNames()) {
1531                 int index = columns.getColumnIndexForName(columnName);
1532                 // We just need to retrieve the first value if there is more than one ...
1533                 tuple[index] = document.get(columnName);
1534             }
1535 
1536             // Set the score column if required ...
1537             if (recordScore) {
1538                 assert scorer != null;
1539                 tuple[scoreIndex] = scorer.score();
1540             }
1541 
1542             // Read the location ...
1543             tuple[locationIndex] = session.readLocation(document);
1544             tuples.add(tuple);
1545         }
1546     }
1547 
1548     /**
1549      * This collector is responsible for loading the value for each of the columns into each tuple array.
1550      */
1551     protected static class FullTextSearchTupleCollector extends TupleCollector {
1552         private final List<Object[]> tuples;
1553         private final FieldSelector fieldSelector;
1554         private final LuceneSearchSession session;
1555         private Scorer scorer;
1556         private IndexReader currentReader;
1557         private int docOffset;
1558 
1559         protected FullTextSearchTupleCollector( LuceneSearchSession session,
1560                                                 List<Object[]> tuples ) {
1561             assert session != null;
1562             assert tuples != null;
1563             this.session = session;
1564             this.tuples = tuples;
1565             this.fieldSelector = LOCATION_FIELDS_SELECTOR;
1566         }
1567 
1568         /**
1569          * @return tuples
1570          */
1571         @Override
1572         public List<Object[]> getTuples() {
1573             return tuples;
1574         }
1575 
1576         /**
1577          * {@inheritDoc}
1578          * 
1579          * @see org.apache.lucene.search.Collector#acceptsDocsOutOfOrder()
1580          */
1581         @Override
1582         public boolean acceptsDocsOutOfOrder() {
1583             return true;
1584         }
1585 
1586         /**
1587          * {@inheritDoc}
1588          * 
1589          * @see org.apache.lucene.search.Collector#setNextReader(org.apache.lucene.index.IndexReader, int)
1590          */
1591         @Override
1592         public void setNextReader( IndexReader reader,
1593                                    int docBase ) {
1594             this.currentReader = reader;
1595             this.docOffset = docBase;
1596         }
1597 
1598         /**
1599          * {@inheritDoc}
1600          * 
1601          * @see org.apache.lucene.search.Collector#setScorer(org.apache.lucene.search.Scorer)
1602          */
1603         @Override
1604         public void setScorer( Scorer scorer ) {
1605             this.scorer = scorer;
1606         }
1607 
1608         /**
1609          * {@inheritDoc}
1610          * 
1611          * @see org.apache.lucene.search.Collector#collect(int)
1612          */
1613         @Override
1614         public void collect( int doc ) throws IOException {
1615             int docId = doc + docOffset;
1616             Object[] tuple = new Object[2];
1617             Document document = currentReader.document(docId, fieldSelector);
1618             // Read the Location ...
1619             tuple[0] = session.readLocation(document);
1620             // And read the score ...
1621             tuple[1] = scorer.score();
1622             // And add the tuple ...
1623             tuples.add(tuple);
1624         }
1625     }
1626 }