LuceneSearcher.java
001 /*
002  * LuceneSearcher.java
003  
004  * Niraj Aswani, 19/March/07
005  
006  * $Id: LuceneSearcher.html,v 1.0 2007/03/19 16:22:01 niraj Exp $
007  */
008 package gate.creole.annic.lucene;
009 
010 import gate.creole.annic.Constants;
011 import gate.creole.annic.Hit;
012 import gate.creole.annic.Pattern;
013 import gate.creole.annic.SearchException;
014 import gate.creole.annic.Searcher;
015 import gate.creole.annic.apache.lucene.document.Document;
016 import gate.creole.annic.apache.lucene.index.IndexReader;
017 import gate.creole.annic.apache.lucene.index.Term;
018 import gate.creole.annic.apache.lucene.index.TermEnum;
019 import gate.creole.annic.apache.lucene.search.BooleanQuery;
020 import gate.creole.annic.apache.lucene.search.Hits;
021 import gate.creole.annic.apache.lucene.search.IndexSearcher;
022 import gate.creole.annic.apache.lucene.search.TermQuery;
023 import gate.persist.LuceneDataStoreImpl;
024 
025 import java.io.File;
026 import java.io.IOException;
027 import java.net.URISyntaxException;
028 import java.net.URL;
029 import java.util.ArrayList;
030 import java.util.HashMap;
031 import java.util.HashSet;
032 import java.util.List;
033 import java.util.Map;
034 import java.util.Set;
035 
036 /**
037  * This class provides the Searching functionality for annic.
038  
039  @author niraj
040  */
041 public class LuceneSearcher implements Searcher {
042 
043   /**
044    * A List of index locations. It allows searching at multiple locations.
045    */
046   private List<String> indexLocations = null;
047 
048   /**
049    * The submitted query.
050    */
051   private String query = null;
052 
053   /**
054    * The number of base token annotations to show in left and right context of
055    * the pattern. By default 5.
056    */
057   private int contextWindow = 5;
058 
059   /**
060    * Found patterns.
061    */
062   private List<Pattern> annicPatterns = new ArrayList<Pattern>();
063 
064   /**
065    * Found annotation types in the annic patterns. The maps keeps record of
066    * found annotation types and features for each of them.
067    */
068   public Map<String, List<String>> annotationTypesMap =
069       new HashMap<String, List<String>>();
070 
071   /**
072    * Search parameters.
073    */
074   private Map<String, Object> parameters = null;
075 
076   /**
077    * Corpus to search in.
078    */
079   private String corpusToSearchIn = null;
080 
081   /**
082    * Annotation set to search in.
083    */
084   private String annotationSetToSearchIn = null;
085 
086   /**
087    * Hits returned by the lucene.
088    */
089   private Hits luceneHits = null;
090 
091   /**
092    * Indicates if the query was to delete certain documents.
093    */
094   private boolean wasDeleteQuery = false;
095 
096   /**
097    * A query can result into multiple queries. For example: (A|B)C is converted
098    * into two queries: AC and AD. For each query a separate thread is started.
099    */
100   private List<LuceneSearchThread> luceneSearchThreads = null;
101 
102   /**
103    * Indicates if the search was successful.
104    */
105   private boolean success = false;
106 
107   /**
108    * Tells which thread to use to retrieve results from.
109    */
110   private int luceneSearchThreadIndex = 0;
111 
112   /**
113    * Tells if we have reached at the end of of found results.
114    */
115   private boolean fwdIterationEnded = false;
116 
117   /**
118    * Used with freq method for statistics.
119    */
120   private LuceneDataStoreImpl datastore;
121 
122   /**
123    * Return the next numberOfHits -1 indicates all
124    */
125   @Override
126   public Hit[] next(int numberOfHitsthrows SearchException {
127 
128     annicPatterns = new ArrayList<Pattern>();
129 
130     if(!success) {
131       this.annicPatterns = new ArrayList<Pattern>();
132       return getHits();
133     }
134 
135     if(fwdIterationEnded) {
136       this.annicPatterns = new ArrayList<Pattern>();
137       return getHits();
138     }
139 
140     try {
141       if(wasDeleteQuery) {
142         List<String> docIDs = new ArrayList<String>();
143         List<String> setNames = new ArrayList<String>();
144         for(int i = 0; i < luceneHits.length(); i++) {
145           Document luceneDoc = luceneHits.doc(i);
146           String documentID = luceneDoc.get(Constants.DOCUMENT_ID);
147           String annotationSetID = luceneDoc.get(Constants.ANNOTATION_SET_ID);
148           int index = docIDs.indexOf(documentID);
149           if(index == -1) {
150             docIDs.add(documentID);
151             setNames.add(annotationSetID);
152           else {
153             if(!setNames.get(index).equals(annotationSetID)) {
154               docIDs.add(documentID);
155               setNames.add(annotationSetID);
156             }
157           }
158         }
159 
160         Hit[] toReturn = new Hit[docIDs.size()];
161         for(int i = 0; i < toReturn.length; i++) {
162           toReturn[inew Hit(docIDs.get(i), setNames.get(i)00"");
163         }
164         return toReturn;
165       }
166 
167       for(; luceneSearchThreadIndex < luceneSearchThreads.size(); luceneSearchThreadIndex++) {
168         LuceneSearchThread lst =
169             luceneSearchThreads.get(luceneSearchThreadIndex);
170         List<Pattern> results = lst.next(numberOfHits);
171         if(results != null) {
172           if(numberOfHits != -1) {
173             numberOfHits -= results.size();
174           }
175 
176           this.annicPatterns.addAll(results);
177           if(numberOfHits == 0) { return getHits()}
178         }
179       }
180 
181       // if we are here, there wer no sufficient patterns available
182       // so what we do is make success to false so that this method
183       // return null on next call
184       fwdIterationEnded = true;
185       return getHits();
186     catch(Exception e) {
187       throw new SearchException(e);
188     }
189   }
190 
191   /**
192    * Method retunrs true/false indicating whether results were found or not.
193    */
194   @SuppressWarnings("unchecked")
195   @Override
196   public boolean search(String query, Map<String, Object> parameters)
197       throws SearchException {
198     luceneHits = null;
199     annicPatterns = new ArrayList<Pattern>();
200     annotationTypesMap = new HashMap<String, List<String>>();
201     luceneSearchThreads = new ArrayList<LuceneSearchThread>();
202     luceneSearchThreadIndex = 0;
203     success = false;
204     fwdIterationEnded = false;
205     wasDeleteQuery = false;
206 
207     if(parameters == null)
208       throw new SearchException("Parameters cannot be null");
209 
210     this.parameters = parameters;
211 
212     /*
213      * lets first check if the query is to search the document names This is
214      * used when we only wants to search for documents stored under the specific
215      * corpus
216      */
217     if(parameters.size() == 2
218         && parameters.get(Constants.INDEX_LOCATION_URL!= null) {
219       String corpusID = (String)parameters.get(Constants.CORPUS_ID);
220       String indexLocation = null;
221       try {
222         indexLocation =
223             new File(
224                 ((URL)parameters.get(Constants.INDEX_LOCATION_URL)).toURI())
225                 .getAbsolutePath();
226       catch(URISyntaxException use) {
227         indexLocation =
228             new File(
229                 ((URL)parameters.get(Constants.INDEX_LOCATION_URL)).getFile())
230                 .getAbsolutePath();
231       }
232 
233       if(corpusID != null && indexLocation != null) {
234         wasDeleteQuery = true;
235         Term term = new Term(Constants.CORPUS_ID, corpusID);
236         TermQuery tq = new TermQuery(term);
237         try {
238           gate.creole.annic.apache.lucene.search.Searcher searcher =
239               new IndexSearcher(indexLocation);
240           // and now execute the query
241           // result of which will be stored in hits
242           luceneHits = searcher.search(tq);
243           success = luceneHits.length() true false;
244           return success;
245         catch(IOException ioe) {
246           ioe.printStackTrace();
247           throw new SearchException(ioe);
248         }
249       }
250     }
251 
252     // check for index locations
253     if(parameters.get(Constants.INDEX_LOCATIONS== null) {
254       String indexLocation;
255       try {
256         indexLocation =
257             new File(((URL)datastore.getIndexer().getParameters()
258                 .get(Constants.INDEX_LOCATION_URL)).toURI()).getAbsolutePath();
259 
260       catch(URISyntaxException use) {
261         indexLocation =
262             new File(((URL)datastore.getIndexer().getParameters()
263                 .get(Constants.INDEX_LOCATION_URL)).getFile())
264                 .getAbsolutePath();
265       }
266       ArrayList<String> indexLocations = new ArrayList<String>();
267       indexLocations.add(indexLocation);
268       parameters.put(Constants.INDEX_LOCATIONS, indexLocations);
269     }
270 
271     indexLocations =
272         new ArrayList<String>(
273             (List<? extends String>)parameters.get(Constants.INDEX_LOCATIONS));
274 
275     if(indexLocations.size() == 0)
276       throw new SearchException("Corpus is not initialized");
277 
278     // check for valid context window
279     if(parameters.get(Constants.CONTEXT_WINDOW== null)
280       throw new SearchException("Parameter " + Constants.CONTEXT_WINDOW
281           " is not provided!");
282 
283     contextWindow =
284         ((Integer)parameters.get(Constants.CONTEXT_WINDOW)).intValue();
285 
286     if(getContextWindow().intValue() <= 0)
287       throw new SearchException("Context Window must be atleast 1 or > 1");
288 
289     if(query == nullthrow new SearchException("Query is not initialized");
290 
291     this.query = query;
292     this.corpusToSearchIn = (String)parameters.get(Constants.CORPUS_ID);
293     this.annotationSetToSearchIn =
294         (String)parameters.get(Constants.ANNOTATION_SET_ID);
295 
296     annicPatterns = new ArrayList<Pattern>();
297     annotationTypesMap = new HashMap<String, List<String>>();
298 
299     luceneSearchThreads = new ArrayList<LuceneSearchThread>();
300 
301     // for different indexes, we create a different instance of
302     // indexSearcher
303     // TODO: is this really useful or used to have several indexLocations ?
304     for(int indexCounter = 0; indexCounter < indexLocations.size(); indexCounter++) {
305       String location = indexLocations.get(indexCounter);
306       // we create a separate Thread for each index
307       LuceneSearchThread lst = new LuceneSearchThread();
308       if(lst.search(query, contextWindow, location, corpusToSearchIn,
309           annotationSetToSearchIn, this)) {
310         luceneSearchThreads.add(lst);
311       }
312     }
313 
314     success = luceneSearchThreads.size() true false;
315     return success;
316   }
317 
318   /**
319    * Gets the submitted query.
320    */
321   @Override
322   public String getQuery() {
323     return this.query;
324   }
325 
326   /**
327    * Gets the number of base token annotations to show in the context.
328    */
329   public Integer getContextWindow() {
330     return new Integer(this.contextWindow);
331   }
332 
333   /**
334    * Gets the found hits (annic patterns).
335    */
336   @Override
337   public Hit[] getHits() {
338     if(annicPatterns == nullannicPatterns = new ArrayList<Pattern>();
339     Hit[] hits = new Hit[annicPatterns.size()];
340     for(int i = 0; i < annicPatterns.size(); i++) {
341       hits[i= annicPatterns.get(i);
342     }
343     return hits;
344   }
345 
346   /**
347    * Gets the map of found annotation types and annotation features. This call
348    * must be invoked only after a call to the
349    * getIndexedAnnotationSetNames(String indexLocation) method. Otherwise this
350    * method doesn't guranttee the correct results. The results obtained has the
351    * following format. Key: CorpusName;AnnotationSetName;AnnotationType Value:
352    * respective features
353    */
354   @Override
355   public Map<String, List<String>> getAnnotationTypesMap() {
356     return annotationTypesMap;
357   }
358 
359   /**
360    * This method returns a set of annotation set names that are indexed. Each
361    * entry has the following format:
362    <p>
363    * corpusName;annotationSetName
364    </p>
365    * where, the corpusName is the name of the corpus the annotationSetName
366    * belongs to.
367    */
368   @Override
369   public String[] getIndexedAnnotationSetNames() throws SearchException {
370     String indexLocation;
371     try {
372       indexLocation =
373           new File(((URL)datastore.getIndexer().getParameters()
374               .get(Constants.INDEX_LOCATION_URL)).toURI()).getAbsolutePath();
375 
376     catch(URISyntaxException use) {
377       indexLocation =
378           new File(((URL)datastore.getIndexer().getParameters()
379               .get(Constants.INDEX_LOCATION_URL)).getFile()).getAbsolutePath();
380     }
381 
382     annotationTypesMap = new HashMap<String, List<String>>();
383     Set<String> toReturn = new HashSet<String>();
384     try {
385       IndexReader reader = IndexReader.open(indexLocation);
386       try {
387 
388         // lets first obtain stored corpora
389         TermEnum terms =
390             reader.terms(new Term(Constants.ANNOTATION_SET_ID, ""));
391         if(terms == null) { return new String[0]}
392 
393         // iterating over terms and finding out names of annotation sets indexed
394         Set<String> annotSets = new HashSet<String>();
395         boolean foundAnnotSet = false;
396         do {
397           Term t = terms.term();
398           if(t == nullcontinue;
399 
400           if(t.field().equals(Constants.ANNOTATION_SET_ID)) {
401             annotSets.add(t.text());
402             foundAnnotSet = true;
403           else {
404             if(foundAnnotSetbreak;
405           }
406         while(terms.next());
407 
408         
409         // we query for each annotation set
410         // and go through docs with that annotation set in them
411         // to see which corpus they belong to.
412         // we could have done the other way round as well (i.e. 
413         // first asking for corpus ids and then obtainin annotation set ids
414         // but not all documents belong to corpora
415         for(String annotSet : annotSets) {
416           Term term = new Term(Constants.ANNOTATION_SET_ID, annotSet);
417           TermQuery tq = new TermQuery(term);
418           try {
419             gate.creole.annic.apache.lucene.search.Searcher searcher =
420                 new IndexSearcher(indexLocation);
421             try {
422               Hits annotSetHits = searcher.search(tq);
423               for(int i = 0; i < annotSetHits.length(); i++) {
424                 Document luceneDoc = annotSetHits.doc(i);
425                 String corpusID = luceneDoc.get(Constants.CORPUS_ID);
426                 if(corpusID == nullcorpusID = "";
427                 toReturn.add(corpusID + ";" + annotSet);
428 
429                 // lets create a boolean query
430                 Term annotSetTerm =
431                     new Term(Constants.ANNOTATION_SET_ID, annotSet);
432                 TermQuery atq = new TermQuery(annotSetTerm);
433 
434                 BooleanQuery bq = new BooleanQuery();
435                 bq.add(tq, true, false);
436                 bq.add(atq, true, false);
437                 gate.creole.annic.apache.lucene.search.Searcher indexFeatureSearcher =
438                     new IndexSearcher(indexLocation);
439                 try {
440                   Hits indexFeaturesHits = searcher.search(bq);
441                   for(int j = 0; j < indexFeaturesHits.length(); j++) {
442                     Document aDoc = indexFeaturesHits.doc(j);
443                     String indexedFeatures =
444                         aDoc.get(Constants.INDEXED_FEATURES);
445                     if(indexedFeatures != null) {
446                       String[] features = indexedFeatures.split(";");
447                       for(String aFeature : features) {
448                         // AnnotationType.FeatureName
449                         int index = aFeature.indexOf(".");
450                         if(index == -1) {
451                           continue;
452                         }
453                         String type = aFeature.substring(0, index);
454                         String featureName = aFeature.substring(index + 1);
455                         String key = corpusID + ";" + annotSet + ";" + type;
456                         List<String> listOfFeatures =
457                             annotationTypesMap.get(key);
458                         if(listOfFeatures == null) {
459                           listOfFeatures = new ArrayList<String>();
460                           annotationTypesMap.put(key, listOfFeatures);
461                         }
462                         if(!listOfFeatures.contains(featureName)) {
463                           listOfFeatures.add(featureName);
464                         }
465                       }
466                     }
467                   }
468                 finally {
469                   indexFeatureSearcher.close();
470                 }
471               }
472             finally {
473               searcher.close();
474             }
475           catch(IOException ioe) {
476             ioe.printStackTrace();
477             throw new SearchException(ioe);
478           }
479         }
480       finally {
481         reader.close();
482       }
483     catch(IOException ioe) {
484       throw new SearchException(ioe);
485     }
486     return toReturn.toArray(new String[0]);
487   }
488 
489   /**
490    * Gets the search parameters set by user.
491    */
492   @Override
493   public Map<String, Object> getParameters() {
494     return parameters;
495   }
496 
497   /**
498    * A Map used for caching query tokens created for a query.
499    */
500   private Map<String, List<String>> queryTokens =
501       new HashMap<String, List<String>>();
502 
503   /**
504    * Gets the query tokens for the given query.
505    */
506   public synchronized List<String> getQueryTokens(String query) {
507     return queryTokens.get(query);
508   }
509 
510   /**
511    * Adds the query tokens for the given query.
512    */
513   public synchronized void addQueryTokens(String query, List<String> queryTokens) {
514     this.queryTokens.put(query, queryTokens);
515   }
516 
517   /**
518    * This method allow exporting results in to the provided file. This method
519    * has not been implemented yet.
520    */
521   @Override
522   public void exportResults(File outputFile) {
523     throw new RuntimeException("ExportResults method is not implemented yet!");
524   }
525 
526   @Override
527   public int freq(String corpusToSearchIn, String annotationSetToSearchIn,
528       String annotationType, String featureName, String value)
529       throws SearchException {
530 
531     String indexLocation;
532     try {
533       indexLocation =
534           new File(((URL)datastore.getIndexer().getParameters()
535               .get(Constants.INDEX_LOCATION_URL)).toURI()).getAbsolutePath();
536 
537     catch(URISyntaxException use) {
538       indexLocation =
539           new File(((URL)datastore.getIndexer().getParameters()
540               .get(Constants.INDEX_LOCATION_URL)).getFile()).getAbsolutePath();
541     }
542     IndexSearcher indexSearcher;
543     try // open the IndexSearcher
544       indexSearcher = new IndexSearcher(indexLocation);
545     catch(IOException e) {
546       e.printStackTrace();
547       return -1;
548     }
549     int result =
550         StatsCalculator.freq(indexSearcher, corpusToSearchIn,
551             annotationSetToSearchIn, annotationType, featureName, value);
552     try // close the IndexSearcher
553       indexSearcher.close();
554     catch(IOException ioe) {
555       ioe.printStackTrace();
556       return -1;
557     }
558     return result;
559   }
560 
561   @Override
562   public int freq(String corpusToSearchIn, String annotationSetToSearchIn,
563       String annotationTypethrows SearchException {
564     return this.freq(corpusToSearchIn, annotationSetToSearchIn, annotationType,
565         null, null);
566   }
567 
568   @Override
569   public int freq(String corpusToSearchIn, String annotationSetToSearchIn,
570       String annotationType, String featureNamethrows SearchException {
571     return this.freq(corpusToSearchIn, annotationSetToSearchIn, annotationType,
572         featureName, null);
573   }
574 
575   @Override
576   public int freq(List<Hit> patternsToSearchIn, String annotationType,
577       String feature, String value, boolean inMatchedSpan, boolean inContext)
578       throws SearchException {
579     return StatsCalculator.freq(patternsToSearchIn, annotationType, feature,
580         value, inMatchedSpan, inContext);
581   }
582 
583   @Override
584   public int freq(List<Hit> patternsToSearchIn, String annotationType,
585       boolean inMatchedSpan, boolean inContextthrows SearchException {
586     return StatsCalculator.freq(patternsToSearchIn, annotationType,
587         inMatchedSpan, inContext);
588   }
589 
590   @Override
591   public Map<String, Integer> freqForAllValues(List<Hit> patternsToSearchIn,
592       String annotationType, String feature, boolean inMatchedSpan,
593       boolean inContextthrows SearchException {
594     return StatsCalculator.freqForAllValues(patternsToSearchIn, annotationType,
595         feature, inMatchedSpan, inContext);
596   }
597 
598   public void setLuceneDatastore(gate.persist.LuceneDataStoreImpl datastore) {
599     this.datastore = datastore;
600   }
601 
602 }