Revision: 14574
          http://gate.svn.sourceforge.net/gate/?rev=14574&view=rev
Author:   valyt
Date:     2011-11-18 15:42:20 +0000 (Fri, 18 Nov 2011)
Log Message:
-----------
[My head hurts. Not being selfish, I'm checking this in so others can enjoy the 
sensation ;) ]

More work done on the ranking query runner:
- background/foreground thread logic now designed
- document IDs are being collected
- most of the major new public API implemented.

Still TODO:
- actual hits collection
- the rest of the public API (direct copy from old implementation?)

Modified Paths:
--------------
    mimir/trunk/mimir-core/src/gate/mimir/search/RankingQueryRunnerImpl.java

Modified: 
mimir/trunk/mimir-core/src/gate/mimir/search/RankingQueryRunnerImpl.java
===================================================================
--- mimir/trunk/mimir-core/src/gate/mimir/search/RankingQueryRunnerImpl.java    
2011-11-18 15:39:17 UTC (rev 14573)
+++ mimir/trunk/mimir-core/src/gate/mimir/search/RankingQueryRunnerImpl.java    
2011-11-18 15:42:20 UTC (rev 14574)
@@ -15,71 +15,161 @@
  */
 package gate.mimir.search;
 
-import gate.mimir.index.IndexException;
 import gate.mimir.search.query.Binding;
 import gate.mimir.search.query.QueryExecutor;
 import gate.mimir.search.query.QueryNode;
 import gate.mimir.search.score.MimirScorer;
-
 import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
 import it.unimi.dsi.fastutil.doubles.DoubleList;
-import it.unimi.dsi.fastutil.ints.Int2IntFunction;
-import it.unimi.dsi.fastutil.ints.IntAVLTreeSet;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
-import it.unimi.dsi.fastutil.ints.IntIterator;
 import it.unimi.dsi.fastutil.ints.IntList;
-import it.unimi.dsi.fastutil.ints.IntSortedSet;
+import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap;
 import it.unimi.dsi.fastutil.objects.ObjectArrayList;
 import it.unimi.dsi.fastutil.objects.ObjectList;
 
 import java.io.IOException;
-import java.io.Serializable;
+import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.SortedMap;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.LinkedBlockingQueue;
 
 import org.apache.log4j.Logger;
 
 /**
- * A QueryRunner implementation that does ranking.
+ * A QueryRunner implementation that can perform ranking.
+ * This query runner has two modes of functioning: ranking and non-ranking, 
+ * depending on whether a {@link MimirScorer} is provided  during construction
+ * or not.  
  */
-public class RankingQueryRunnerImpl implements Runnable {
+public class RankingQueryRunnerImpl {
   
+  private static final Runnable NO_MORE_JOBS = new Runnable(){ 
+    public void run() {}
+  };
+  
+  protected class BackgroundRunner implements Runnable {
+    @Override
+    public void run() {
+      try {
+        while(true) {
+          Runnable job = backgroundTasks.take();
+          if(job == NO_MORE_JOBS) break;
+          else  job.run();
+        }
+      } catch(InterruptedException e) {
+        Thread.currentThread().interrupt();
+        e.printStackTrace();
+      }
+    }
+  }
+   
+  
   /**
-   * When doing ranking, this class is used to delegate the iteration of 
-   * document IDs to an IntIterator that is limited to a finite set of 
documents
-   * that have just been ranked. When that iterator is emptied, the 
-   * {@link RankingQueryRunnerImpl#documentsById} field is nullified before 
-   * hasNext() returns false, to indicate that more documents may be available.
-   */
-  protected class RankedDocIdIterator implements IntIterator {
+   * Collects the document hits (i.e. {@link Binding}s) for the documents 
+   * between the two provided ranks (indexes in the {@link #documentsOrder} 
+   * list. If ranking is not being performed ( {@link #documentsOrder} is
+   * <code>null</null>, then the indexes are used against the 
+   * {@link #documentIds} list.
+   * 
+   * This is the only actor that writes to the {@link #documentHits} list.
+   */  
+  protected class HitsCollector implements Runnable {
+    int start;
+    int end;
     
-    public RankedDocIdIterator(IntIterator underlyingIterator) {
-      this.underlyingIterator = underlyingIterator;
+    public HitsCollector(int rangeStart, int rangeEnd) {
+      this.start = rangeStart;
+      this.end = rangeEnd;
     }
-
-    protected IntIterator underlyingIterator;
-
-    public boolean hasNext() {
-      return underlyingIterator.hasNext();
+    
+    @Override
+    public void run() {
+      // TODO Auto-generated method stub
+      if(documentScores != null) {
+        // we're ranking -> first calculate the range of documents in ID order
+        
+      }
+      
     }
-
-    public Integer next() {
-      return underlyingIterator.next();
+  }
+  
+  
+  /**
+   * The first action started when a new {@link RankingQueryRunnerImpl} is 
+   * created. It performs the following actions:
+   * <ul>
+   *   <li>collects all document IDs in 
+   *   {@link RankingQueryRunnerImpl#documentIds}</li>
+   *   <li>if ranking enabled
+   *     <ul>
+   *     <li>it collects all document scores
+   *     </ul>
+   *   </li>  
+   *   <li>if ranking not enabled
+   *     <ul>
+   *       <li>it collects the document hits for the first 
+   *       block of documents</li>
+   *     </ul>
+   *   </li>
+   *   <li>If ranking enabled, after all document IDs are obtained, it starts 
+   *   the work for ranking the first block of documents (which, upon 
+   *   completion, will also start a background job to collect all the hits 
for 
+   *   that block).</li>  
+   * </ul>
+   */
+  protected class DocIdsCollector implements Runnable {
+    @Override
+    public void run() {
+      try{
+        // collect all documents and their scores
+        final boolean scoring = scorer != null;
+        if(scoring) scorer.wrap(queryExecutor);
+        int docId = scoring ? scorer.nextDocument(-1) : 
queryExecutor.nextDocument(-1);
+        if(!scoring) { // then also collect some hits
+          synchronized(hitCollectors) {
+           hitCollectors.put(new int[]{0, docBlockSize}, 
+             new FutureTask<Object>(this, null));
+          }
+        }
+        while(docId >= 0) {
+          documentIds.add(docId);
+          if(scoring){
+            documentScores.add(scorer.score());
+            documentHits.add(null);
+          } else {
+            // not scoring: also collect the hits for the first block of 
documents
+            if(docId < docBlockSize) {
+              ObjectList<Binding> hits = new ObjectArrayList<Binding>();
+              Binding hit = queryExecutor.nextHit();
+              while(hit != null) {
+                hits.add(hit);
+                hit = queryExecutor.nextHit();
+              }
+              documentHits.add(hits);
+            } else {
+              documentHits.add(null);
+            }
+          }
+          docId = scoring ? scorer.nextDocument(-1) : 
queryExecutor.nextDocument(-1);
+        }
+        allDocIdsCollected = true;
+        if(scoring) {
+          // now rank the first batch of documents
+          // this will also start a second background job to collect the hits
+          rankDocuments(queryExecutor.getQueryEngine().getRankingDocCount() 
-1);
+        }
+      }catch (IOException e) {
+        logger.error("Exception while collecting document IDs", e);
+        try {
+          close();
+        } catch(IOException e1) {
+          logger.error("Exception while closing, after exception.", e1);
+        }
+      }
     }
-
-    public void remove() {
-      underlyingIterator.remove();
-    }
-
-    public int nextInt() {
-      return underlyingIterator.nextInt();
-    }
-
-    public int skip(int n) {
-      return underlyingIterator.skip(n);
-    }
-    
   }
   
   protected Logger logger =  Logger.getLogger(RankingQueryRunnerImpl.class);
@@ -95,6 +185,12 @@
   protected MimirScorer scorer;
 
   /**
+   * The number of documents to be ranked (of have their hits collected) as a 
+   * block.
+   */
+  protected int docBlockSize;
+  
+  /**
    * The document IDs for the documents found to contain hits. This list is
    * sorted in ascending documentID order.
    */
@@ -111,8 +207,7 @@
    * The sets of hits for each returned document. This data structure is 
lazily 
    * built, so some elements may be null. 
    */
-  protected ObjectList<Binding[]> documentHits;
-  
+  protected ObjectList<List<Binding>> documentHits;
 
   /**
    * The order the documents should be returned in (elements in this list are 
@@ -121,18 +216,25 @@
   protected IntList documentsOrder;
   
   /**
-   * An iterator supplying documentIDs in ascending order. These are used when 
-   * collecting the hits.
+   * Data structure holding references to {@link Future}s that are currently 
+   * working (or have worked) on collecting hits for a range of document 
+   * indexes.
    */
-  protected IntIterator documentsById;
+  protected SortedMap<int[], Future<?>> hitCollectors;
   
   /**
-   * The thread used for executing the query. This is a separate thread from 
one 
-   * that created the query runner.
+   * The background thread used for collecting hits.
    */
   protected Thread runningThread;
   
   /**
+   * A queue with tasks to be executed by the background thread. 
+   */
+  protected BlockingQueue<Runnable> backgroundTasks;
+  
+  protected volatile boolean allDocIdsCollected = false;
+  
+  /**
    * Creates a query runner in ranking mode.
    * @param qNode the {@link QueryNode} for the query being executed.
    * @param scorer the {@link MimirScorer} to use for ranking.
@@ -142,36 +244,45 @@
   public RankingQueryRunnerImpl(QueryExecutor executor, MimirScorer scorer) 
throws IOException {
     this.queryExecutor = executor;
     this.scorer = scorer;
-    documentsById = null;
-    // start the search
-    getMoreHits();
-  }
-  
-  
-  /* (non-Javadoc)
-   * @see gate.mimir.search.QueryRunner#getMoreHits()
-   */
-  protected synchronized void getMoreHits() throws IOException {
-    if(runningThread != null){
-      //we're already running -> ignore
-      return;
+    docBlockSize = queryExecutor.getQueryEngine().getRankingDocCount();
+    documentIds = new IntArrayList();
+    documentHits = new ObjectArrayList<List<Binding>>();
+    if(scorer != null) {
+      documentScores = new DoubleArrayList();
+      documentsOrder = new IntArrayList(
+        queryExecutor.getQueryEngine().getRankingDocCount());
     }
-    // get a thread from the executor, if one exists
+    hitCollectors = new Object2ObjectAVLTreeMap<int[], Future<?>>(
+        new Comparator<int[]>(){
+          @Override
+          public int compare(int[] o1, int[] o2) { return o1[0] - o2[0]; }
+        });
+    // start the background thread
+    backgroundTasks = new LinkedBlockingQueue<Runnable>();
+    Runnable backgroundRunner = new BackgroundRunner();
+    //get a thread from the executor, if one exists
     if(queryExecutor.getQueryEngine().getExecutor() != null){
-      queryExecutor.getQueryEngine().getExecutor().execute(this);  
+      queryExecutor.getQueryEngine().getExecutor().execute(backgroundRunner);
     }else{
-      new Thread(this, getClass().getName()).start();
+      new Thread(backgroundRunner, getClass().getName()).start();
     }
+
+    // queue a job for collecting all document ids
+    try {
+      backgroundTasks.put(new DocIdsCollector());
+    } catch(InterruptedException e) {
+      Thread.currentThread().interrupt();
+      logger.error("Could not queue a background task.", e);
+    }
   }
   
-  
   /**
-   * Gets the number of documents found to contain hits. If the search has not
-   * yet completed, then -1 is returned.
+   * Gets the number of result documents. If the search has not yet completed, 
+   * then -1 is returned.
    * @return
    */
   public int getDocumentsCount() {
-    if(queryExecutor == null) return documentIds.size();
+    if(allDocIdsCollected) return documentIds.size();
     else return -1;
   }
 
@@ -186,57 +297,99 @@
   }
   
   /**
-   * Gets the ID of a document found to contain hits.
-   * @param index the index of the desired document in the list of documents. 
+   * Gets the ID of a result document.
+   * @param rank the index of the desired document in the list of documents. 
    * This should be a value between 0 and {@link #getDocumentsCount()} -1.
    *  
+   * If the requested document position has not yet been ranked (i.e. we know 
+   * there is a document at that position, but we don't yet know which one) 
then 
+   * the necessary ranking is performed before this method returns. 
+   *
    * @return an int value, representing the ID of the requested document.
    * @throws IndexOutOfBoundsException is the index provided is less than 
zero, 
    * or greater than {@link #getDocumentsCount()} -1.
+   * @throws IOException 
    */
-  public int getDocumentID(int index) throws IndexOutOfBoundsException {
-    // TODO: check position index has been ranked yet
+  public int getDocumentID(int rank) throws IndexOutOfBoundsException, 
IOException {
+    return documentIds.getInt(getDocumentIndex(rank));
+  }
+  
+  /**
+   * Retrieves the hits withing a given result document.
+   * @param rank the index of the desired document in the list of documents.
+   * This should be a value between 0 and {@link #getDocumentsCount()} -1.
+   * 
+   * This method call waits until the requested data is available before 
+   * returning (document hits are being collected by a background thread).
+   * 
+   * @return
+   * @throws IOException 
+   * @throws IndexOutOfBoundsException 
+   */
+  public List<Binding> getDocumentHits(int rank) throws 
IndexOutOfBoundsException, IOException {
+    int documentIndex = getDocumentIndex(rank);
+    List<Binding> hits = documentHits.get(documentIndex);
+    if(hits == null) {
+      // hits not collected yet
+      try {
+        // find the Future working on it, or start a new one, 
+        // then wait for it to complete
+        collectHits(new int[]{documentIndex, documentIndex}).get();
+        hits = documentHits.get(documentIndex);
+      } catch(Exception e) {
+        logger.error("Exception while waiting for hits collection", e);
+        throw new RuntimeException(
+          "Exception while waiting for hits collection", e); 
+      }
+    }
+    return hits;
+  }
+  
+  /**
+   * Given a document rank, return its index in the {@link #documentIds} list.
+   * @param rank
+   * @return
+   * @throws IOException, IndexOutOfBoundsException 
+   */
+  protected int getDocumentIndex(int rank) throws IOException, 
IndexOutOfBoundsException {
+    int maxIndex = documentIds.size();
+    if(rank >= maxIndex) throw new IndexOutOfBoundsException(
+      "Document index too large (" + rank + " > " + maxIndex + ".");
     if(documentsOrder != null) {
-      return documentIds.getInt(documentsOrder.getInt(index));
+      // we're in ranking mode
+      if(rank >= documentsOrder.size()) {
+        // document exists, but has not been ranked yet
+        rankDocuments(rank);
+      }
+      return documentsOrder.getInt(rank);
     } else {
-      return documentIds.getInt(index);  
+      return rank;
     }
   }
   
   /**
-   * Creates an {@link IntIterator} used for enumerating the documents in the
-   * correct order for returning to the user.
-   * If no more documents are available, this should return null
-   * @return
+   * Ranks some more documents (i.e. adds more entries to the 
+   * {@link #documentsOrder} list, making sure that the document at provided 
+   * index is included.
+   * This is the only method that writes to the {@link #documentsOrder} list.
+   * This method is executed synchronously in the client thread.
+   *  
+   * @param index
    * @throws IOException 
    */
-  protected IntIterator getDocumentIterator() throws IOException {
-    if(scorer != null) {
-      // we're doing ranking
-      if(documentIds == null) {
-        // first stage: collect all documents and their scores
-        documentIds = new IntArrayList();
-        documentScores = new DoubleArrayList();
-        documentHits = new ObjectArrayList<Binding[]>();
-        documentsOrder = new IntArrayList(
-          queryExecutor.getQueryEngine().getRankingDocCount());
-        
-        scorer.wrap(queryExecutor);
-        int docId = scorer.nextDocument(-1);
-        while(docId >= 0) {
-          documentIds.add(docId);
-          documentScores.add(scorer.score());
-          documentHits.add(null);
-          docId = scorer.nextDocument(-1);
-        }
+  protected void rankDocuments(int index) throws IOException {
+    synchronized(documentsOrder) {
+      // rank some documents
+      int rankRangeStart = documentsOrder.size();
+      int rankRangeEnd = index;
+      if(rankRangeEnd - rankRangeStart < 
+          queryExecutor.getQueryEngine().getRankingDocCount()) {
+        // extend the size of the chunk of documents to be ranked
+        rankRangeEnd = rankRangeStart + 
+            queryExecutor.getQueryEngine().getRankingDocCount(); 
       }
-      // collect some more ranked documents
+      int documentsOrderWriteIndex = rankRangeStart;
       
-      int rankRangeStart = documentsOrder.size();
-      int rankRangeEnd = documentsOrder.size() + 
-          queryExecutor.getQueryEngine().getRankingDocCount();
-      int docsByRankWriteIndex = rankRangeStart;
-      
       // the document with the minimum score already ranked.
       int smallestOldScoreDocId = rankRangeStart > 0 ? 
         documentIds.getInt(documentsOrder.getInt(rankRangeStart -1))
@@ -245,8 +398,6 @@
       double smallestOldScore = rankRangeStart > 0 ? 
           documentScores.getDouble(documentsOrder.getInt(rankRangeStart -1))
           : -1;
-      // the documentIds for newly ranked documents
-      IntSortedSet newDocuments = new IntAVLTreeSet();
       // now collect some more documents
       for(int i = 0; i < documentIds.size(); i++) {
         int documentId = documentIds.getInt(i);
@@ -264,18 +415,17 @@
         // smaller score than the maximum permitted score (i.e. it has not 
         // already been ranked)., or
         // - it's a new document with the same score as the largest permitted 
score
-        if(docsByRankWriteIndex < rankRangeEnd 
+        if(documentsOrderWriteIndex < rankRangeEnd 
            || 
            (documentScore > smallestNewScore && 
                (smallestOldScore < 0 || documentScore < smallestOldScore)) 
            ||
            documentScore == smallestOldScore && documentId != 
smallestOldScoreDocId) {
-          if(docsByRankWriteIndex == rankRangeEnd) {
+          if(documentsOrderWriteIndex == rankRangeEnd) {
             // we need to remove the  newly ranked document 
             // with the smallest score
-            docsByRankWriteIndex--;
-            int oldDocIndex = documentsOrder.removeInt(docsByRankWriteIndex);
-            newDocuments.remove(documentIds.getInt(oldDocIndex));
+            documentsOrderWriteIndex--;
+            documentsOrder.removeInt(documentsOrderWriteIndex);
           }
           // find the rank for the new doc
           int rank = rankRangeStart;
@@ -284,46 +434,67 @@
             rank++;
           }
           documentsOrder.add(rank, i);
-          newDocuments.add(documentId);
-          docsByRankWriteIndex++;
+          documentsOrderWriteIndex++;
         }
       }
-      if(newDocuments.isEmpty()){
-        return null;
-      } else {
-        return new RankedDocIdIterator(newDocuments.iterator());
+      // start collecting the hits for the newly ranked documents (in a new 
thread)
+      if(documentsOrderWriteIndex > rankRangeStart){
+        collectHits(new int[] {rankRangeStart, documentsOrderWriteIndex});
       }
-    } else {
-      // we're not doing scoring, simply use the queryExecutor as an 
intIterator
-      return queryExecutor;
     }
   }
   
-  public void run() {
-    //store the running thread
-    synchronized(this) {
-      if(runningThread != null){
-        //some task is already running
-        return;
+  /**
+   * Makes sure all the documents in the specified range are queued for hit 
+   * collection. 
+   * @param interval the interval specified by 2 document ranks
+   * @return the future that has been queued for collecting the hits.
+   */
+  protected Future collectHits(int[] interval) {
+    // expand the interval to block size
+    if(interval[1] - interval[0] < docBlockSize) {
+      interval[0] -= docBlockSize / 2;
+      interval[1] += docBlockSize / 2;
+    }
+    HitsCollector hitsCollector = null;
+    Future<?> future;
+    synchronized(hitCollectors) {
+      SortedMap<int[], Future<?>> headMap = hitCollectors.headMap(interval); 
+      int[] previousInterval = headMap.isEmpty() ? new int[]{0,0} : 
+          headMap.lastKey();
+      if(previousInterval[1] >= interval[1]) {
+        // we're part of previous interval
+        future = hitCollectors.get(previousInterval);
+      } else {
+        // calculate an appropriate interval to collect hits for
+        SortedMap<int[], Future<?>> tailMap = hitCollectors.tailMap(interval);
+        int[] followingInterval = tailMap.isEmpty() ? 
+          new int[]{Integer.MAX_VALUE, Integer.MAX_VALUE} : tailMap.firstKey();
+        int start = Math.max(previousInterval[1], interval[0]);
+        int end = Math.min(followingInterval[0], interval[1]);
+        hitsCollector = new HitsCollector(start, end);
+        future = new FutureTask(hitsCollector, null);
+        hitCollectors.put(new int[]{start, end}, future);
       }
-      runningThread = Thread.currentThread();  
     }
-    try {
-      if(documentsById == null) {
-        documentsById = getDocumentIterator();
+    if(hitsCollector != null) {
+      try {
+        backgroundTasks.put(hitsCollector);
+      } catch(InterruptedException e) {
+        logger.error("Error while queuing background work", e);
+        throw new RuntimeException("Error while queuing background work", e);
       }
-      // collect the hits
-      while(documentsById != null) {
-        
-      }
-    } catch(IOException e) {
-      //something went bad!
-      logger.error("IOException during search!", e);
-    }finally{
-      //this search stage has completed -> clear the running thread
-      synchronized(this) {
-        runningThread = null;  
-      }
-    } 
+    }
+    return future;
   }
-}
+  
+  public void close() throws IOException {
+    queryExecutor.close();
+    scorer = null;
+    try {
+      backgroundTasks.put(NO_MORE_JOBS);
+    } catch(InterruptedException e) {
+      // ignore
+    }
+  } 
+}
\ No newline at end of file

This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.


------------------------------------------------------------------------------
All the data continuously generated in your IT infrastructure 
contains a definitive record of customers, application performance, 
security threats, fraudulent activity, and more. Splunk takes this 
data and makes sense of it. IT sense. And common sense.
http://p.sf.net/sfu/splunk-novd2d
_______________________________________________
GATE-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/gate-cvs

Reply via email to