]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.db.indexing/src/org/simantics/db/indexing/IndexedRelationsSearcherBase.java
Added new field TypeId to dependency index for exact type searching
[simantics/platform.git] / bundles / org.simantics.db.indexing / src / org / simantics / db / indexing / IndexedRelationsSearcherBase.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2015 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *     Semantum Oy - Fix for simantics issue #6053
12  *******************************************************************************/
13 package org.simantics.db.indexing;
14
15 import java.io.IOException;
16 import java.nio.file.Files;
17 import java.nio.file.Path;
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.concurrent.CompletableFuture;
25 import java.util.concurrent.Semaphore;
26 import java.util.concurrent.atomic.AtomicReference;
27
28 import org.apache.lucene.document.Document;
29 import org.apache.lucene.document.DocumentStoredFieldVisitor;
30 import org.apache.lucene.document.Field;
31 import org.apache.lucene.document.FieldType;
32 import org.apache.lucene.document.LongField;
33 import org.apache.lucene.document.TextField;
34 import org.apache.lucene.index.CorruptIndexException;
35 import org.apache.lucene.index.DirectoryReader;
36 import org.apache.lucene.index.FieldInfo;
37 import org.apache.lucene.index.IndexNotFoundException;
38 import org.apache.lucene.index.IndexReader;
39 import org.apache.lucene.index.IndexWriter;
40 import org.apache.lucene.index.IndexWriterConfig;
41 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
42 import org.apache.lucene.index.IndexableField;
43 import org.apache.lucene.index.StoredFieldVisitor;
44 import org.apache.lucene.index.Term;
45 import org.apache.lucene.queryparser.classic.ParseException;
46 import org.apache.lucene.search.IndexSearcher;
47 import org.apache.lucene.search.MatchAllDocsQuery;
48 import org.apache.lucene.search.Query;
49 import org.apache.lucene.search.ScoreDoc;
50 import org.apache.lucene.search.TermQuery;
51 import org.apache.lucene.search.TopDocs;
52 import org.apache.lucene.store.Directory;
53 import org.apache.lucene.store.FSDirectory;
54 import org.apache.lucene.util.Version;
55 import org.eclipse.core.runtime.IProgressMonitor;
56 import org.eclipse.core.runtime.SubMonitor;
57 import org.simantics.databoard.util.ObjectUtils;
58 import org.simantics.db.ReadGraph;
59 import org.simantics.db.RequestProcessor;
60 import org.simantics.db.Resource;
61 import org.simantics.db.Session;
62 import org.simantics.db.common.request.SafeName;
63 import org.simantics.db.common.utils.NameUtils;
64 import org.simantics.db.exception.DatabaseException;
65 import org.simantics.db.indexing.internal.IndexingJob;
66 import org.simantics.db.layer0.adapter.GenericRelation;
67 import org.simantics.db.request.Read;
68 import org.simantics.db.service.CollectionSupport;
69 import org.simantics.db.service.SerialisationSupport;
70 import org.simantics.utils.FileUtils;
71 import org.simantics.utils.datastructures.Pair;
72 import org.simantics.utils.threads.ThreadUtils;
73 import org.slf4j.Logger;
74
75 import gnu.trove.map.hash.THashMap;
76
77 /**
78  * @author Tuukka Lehtonen
79  * @author Antti Villberg
80  */
81 abstract public class IndexedRelationsSearcherBase {
82
83     protected enum State {
84         // No index is available
85         NONE, 
86         // An index is available, but there is a problem with it
87         PROBLEM, 
88         // An index is available but no reader or writer is ready
89         READY,
90         // A reader is ready
91         READ, 
92         // A writer (and a reader) is ready
93         WRITE
94     }
95     
96     private State state = State.READY;
97     private Throwable exception;
98     
99     public Throwable getException() {
100         return exception;
101     }
102
103     public void setProblem(Throwable t) {
104         if (t != null)
105             getLogger().error("Setting problem for {} and previous state {}", this, this.state, t);
106         this.state = State.PROBLEM;
107         this.exception = t;
108     }
109     
110     public void setNone() {
111         this.state = State.NONE;
112     }
113     
114     public void setReady() {
115         this.state = State.READY;
116     }
117     
118     protected boolean checkState(State state) {
119         return this.state == state;
120     }
121     
122     protected void assertState(State state) throws AssertionError {
123
124         if(this.state != state) throw new AssertionError("Illegal state, expected " + state.name() + " but was in " + this.state.name());
125         
126     }
127     
128     public void changeState(IProgressMonitor monitor, Session session, State state) {
129         changeState(monitor, session, state, 0);
130     }
131
132     protected void changeState(IProgressMonitor monitor, Session session, State state, int depth) {
133
134         if (this.state == state) {
135             if (getLogger().isDebugEnabled())
136                 getLogger().debug("Trying to change state {} to the same as previous state {} in depth {} with {}", state, this.state, depth, this);
137             return;
138         }
139
140         if (IndexPolicy.TRACE_INDEX_MANAGEMENT)
141                 System.err.println("Index state " + this.state.name() + " => " + state.name() + " " + this);
142
143         // Check transitions
144         
145         // Try to exit problem state
146         if (State.PROBLEM == this.state && depth > 0) {
147             getLogger().info("Try to exit problem state for {} and state {}", this, state);
148                 Throwable t = bestEffortClear(monitor, session);
149                 if(t != null) {
150                     getLogger().error("Best effort clear has failed for state {} and this {}", state, this, t);
151                                 exception = t;
152                                 return;
153                 }
154                 // Managed to get into initial state
155                 this.state = State.NONE;
156                 getLogger().info("Managed to get into initial state {}", this.state);
157                 return;
158         }
159
160         // Cannot move into read from no index
161         if (State.NONE ==  this.state && State.READ == state) {
162             if (getLogger().isDebugEnabled())
163                 getLogger().debug("Cannot move into read from no index in {} with state {}", this, state);
164             return;
165         }
166         // Cannot move into write from no index
167         if (State.NONE ==  this.state && State.WRITE == state) {
168             if (getLogger().isDebugEnabled())
169                 getLogger().debug("Cannot move into write from no index in {} with state {}", this, state);
170             return;
171         }
172         
173                 boolean success = false;
174
175         try {
176
177                 if (searcher != null) {
178                         searcher = null;
179                 }
180                 if (reader != null) {
181                         reader.close();
182                         reader = null;
183                 }
184                         closeWriter(writer);
185                         directory = null;
186                         
187                         success = true;
188
189                 // Enter new state
190                 if (State.READ == state || State.WRITE == state) {
191                         
192                         success = false;
193                         
194                         boolean forWriting = State.WRITE == state;
195
196                         if (directory != null)
197                                 throw new IllegalStateException(getDescriptor() + "Index already loaded");
198
199                         SubMonitor mon = SubMonitor.convert(monitor, 100);
200
201                         mon.beginTask("Loading index", 100);
202
203                         if (IndexPolicy.TRACE_INDEX_LOAD)
204                     System.out.println(getDescriptor() + "Loading Lucene index from " + indexPath + " for " + (forWriting ? "writing" : "reading"));
205
206                 long start = System.nanoTime();
207
208                 directory = getDirectory(session);
209
210                 if (forWriting) {
211                     // Never overwrite an index that is about to be loaded.
212                     // TODO: could use OpenMode.CREATE_OR_APPEND but must test first
213                     IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_9, Queries.getAnalyzer()).setOpenMode(OpenMode.APPEND);
214                     try {
215                         // FIXME: platform #4676
216                         writer = new IndexWriter(directory, config);
217                     } catch (IndexNotFoundException e) {
218                         // There was no pre-existing index on disk. Create it now.
219                         writer = new IndexWriter(directory, config.setOpenMode(OpenMode.CREATE));
220                         writer.commit();
221                     }
222                     reader = DirectoryReader.open(directory);
223                     searcher = new IndexSearcher(reader);
224                 } else {
225                     reader = DirectoryReader.open(directory);
226                     searcher = new IndexSearcher(reader);
227                 }
228
229                 long end = System.nanoTime();
230
231                 mon.worked(100);
232
233                 if (IndexPolicy.PERF_INDEX_LOAD) {
234                     double time = (end - start) * 1e-6;
235                     System.out.println(getDescriptor() + "Loaded Lucene index from " + indexPath + " for " + (forWriting ? "writing" : "reading") + " in " + time + " ms");
236                 }
237
238                 success = true;
239                 
240                 }
241                 
242         } catch (Throwable t) {
243                 setProblem(t);
244         } finally {
245
246                 if(!success) {
247                         this.state = State.PROBLEM;
248                         changeState(monitor, session, State.NONE, depth+1);
249                         return;
250                 }
251
252         }
253
254         this.state = state;
255         
256     }
257
258     public static final FieldType STRING_TYPE = new FieldType();
259
260     static {
261       STRING_TYPE.setIndexed(true);
262       STRING_TYPE.setStored(true);
263       STRING_TYPE.setTokenized(true);
264       STRING_TYPE.freeze();
265     }
266
267     protected static Field makeField(String fieldName, String fieldClass) throws DatabaseException {
268         switch (fieldClass) {
269         case "Long":   return new LongField(fieldName, 0L, Field.Store.YES);
270         case "String": return new Field    (fieldName, "", STRING_TYPE);
271         case "Text":   return new TextField(fieldName, "", Field.Store.YES);
272         default:
273             throw new DatabaseException("Can only index Long, String and Text fields, encountered field type " + fieldClass);
274         }
275     }
276
277     protected static Field[] makeFieldsForRelation(GenericRelation r, int boundLength, Document document) throws DatabaseException {
278         Pair<String, String>[] fields = r.getFields();
279         Field[] fs = new Field[Math.max(0, fields.length - boundLength)];
280         for (int i = boundLength; i < fields.length; i++) {
281             Field f = makeField(fields[i].first, fields[i].second);
282             fs[i - boundLength] = f;
283             if (document != null)
284                 document.add(f);
285         }
286         return fs;
287     }
288
289     void insertIndex(IProgressMonitor monitor, GenericRelation r, int boundLength, Collection<Object[]> documentsData)
290     throws CorruptIndexException, IOException, DatabaseException {
291         assertAccessOpen(true);
292
293         if (IndexPolicy.TRACE_INDEX_UPDATE)
294             System.out.println(getDescriptor() + "Inserting " + documentsData.size() + " documents into index at " + indexPath);
295
296         long start = 0, end = 0;
297         if (IndexPolicy.PERF_INDEX_UPDATE)
298             start = System.nanoTime();
299
300         try {
301             Document document = new Document();
302             Field[] fs = makeFieldsForRelation(r, boundLength, document);
303
304             for (Object[] documentData : documentsData) {
305                 if (setFields(fs, documentData) == null)
306                     continue;
307
308                 if (IndexPolicy.TRACE_INDEX_UPDATE)
309                     System.out.println(getDescriptor() + "Inserting document " + document);
310
311                 writer.addDocument(document);
312             }
313
314             if (IndexPolicy.PERF_INDEX_UPDATE) {
315                 end = System.nanoTime();
316                 double ms = (end - start) * 1e-6;
317                 System.out.println(getDescriptor() + "Inserted " + documentsData.size() + " documents into index at " + indexPath + " in " + ms + " ms");
318             }
319
320         } finally {
321         }
322     }
323
324     void removeIndex(IProgressMonitor monitor, GenericRelation r, RequestProcessor processor, String key, Collection<Object> keyValues) throws DatabaseException, CorruptIndexException, IOException {
325         assertAccessOpen(true);
326
327         if (IndexPolicy.TRACE_INDEX_UPDATE)
328             System.out.println(getDescriptor() + "Removing " + keyValues.size() + " documents from index at " + indexPath);
329
330         long start = 0, end = 0;
331         if (IndexPolicy.PERF_INDEX_UPDATE)
332             start = System.nanoTime();
333
334         try {
335             for (Object keyValue : keyValues) {
336                 Term removedTerm = null;
337                 if (keyValue instanceof Long) {
338                     removedTerm = IndexUtils.longTerm(key, (Long) keyValue);
339                 } else if (keyValue instanceof String) {
340                     removedTerm = new Term(key, (String) keyValue);
341                 } else {
342                     // FIXME: should throw an exception for illegal input data but this would leave the index in an incoherent state
343                     getLogger().error("Attempting to remove document from index of {} with key {} and unrecognized key value type {} : {}", input, key, keyValue, keyValue != null ? keyValue.getClass() : "null");
344                     continue;
345                 }
346
347                 if (IndexPolicy.TRACE_INDEX_UPDATE)
348                     System.out.println(getDescriptor() + "Removing document with key " + removedTerm);
349                 writer.deleteDocuments(removedTerm);
350             }
351
352             if (IndexPolicy.PERF_INDEX_UPDATE) {
353                 end = System.nanoTime();
354                 double ms = (end - start) * 1e-6;
355                 System.out.println(getDescriptor() + "Removed " + keyValues.size() + " documents from index at " + indexPath + " in " + ms + " ms");
356             }
357
358         } finally {
359         }
360     }
361
362     void removeIndex(IProgressMonitor monitor) throws DatabaseException, CorruptIndexException, IOException {
363         assertAccessOpen(true);
364
365         long start = 0, end = 0;
366         if (IndexPolicy.PERF_INDEX_UPDATE)
367             start = System.nanoTime();
368
369         try {
370
371             writer.deleteAll();
372
373             if (IndexPolicy.PERF_INDEX_UPDATE) {
374                 end = System.nanoTime();
375                 double ms = (end - start) * 1e-6;
376                 System.out.println(getDescriptor() + "Removed all documents from index at " + indexPath + " in " + ms + " ms");
377             }
378
379         } finally {
380         }
381     }
382     
383     boolean replaceIndex(IProgressMonitor monitor, String key, Collection<Object> keyValues, GenericRelation r, int boundLength, Collection<Object[]> documentsData) throws CorruptIndexException, IOException, DatabaseException {
384
385         boolean didReplace = false;
386         
387         assertAccessOpen(true);
388         if (keyValues.size() != documentsData.size())
389             throw new IllegalArgumentException("keyValues size does not match documents data size, " + keyValues.size() + " <> " + documentsData.size());
390
391         if (IndexPolicy.TRACE_INDEX_UPDATE)
392             System.out.println(getDescriptor() + "Replacing " + keyValues.size() + " documents from index at " + indexPath);
393
394         long start = 0, end = 0;
395         if (IndexPolicy.PERF_INDEX_UPDATE)
396             start = System.nanoTime();
397
398         try {
399             Iterator<Object> keyIt = keyValues.iterator();
400             Iterator<Object[]> documentDataIt = documentsData.iterator();
401
402             Document document = new Document();
403             Field[] fs = makeFieldsForRelation(r, boundLength, document);
404
405             nextDocument:
406                 while (keyIt.hasNext()) {
407                     Object keyValue = keyIt.next();
408                     Object[] documentData = documentDataIt.next();
409
410                     Term removedTerm = null;
411                     if (keyValue instanceof Long) {
412                         removedTerm = IndexUtils.longTerm(key, (Long) keyValue);
413                     } else if (keyValue instanceof String) {
414                         removedTerm = new Term(key, (String) keyValue);
415                     } else {
416                         // FIXME: should throw an exception for illegal input data but this would leave the index in an incoherent state
417                         System.err.println("[" + getClass().getSimpleName() + "] Unrecognized document key to remove '" + keyValue + "', only " + String.class + " and " + Resource.class + " are supported.");
418                         continue nextDocument;
419                     }
420
421                     if (setFields(fs, documentData) == null)
422                         continue nextDocument;
423
424                     if (IndexPolicy.TRACE_INDEX_UPDATE)
425                         System.out.println(getDescriptor() + "Replacing document with key " + removedTerm + " with " + document);
426
427                     boolean done = false;
428                     if(requireChangeInfoOnReplace()) {
429                             TopDocs exist = searcher.search(new TermQuery(removedTerm), null, 2);
430                             if(exist.scoreDocs.length == 1) {
431                                 Document doc = reader.document(exist.scoreDocs[0].doc);
432                                 if(!areSame(doc, document)) {
433                                     writer.deleteDocuments(removedTerm);
434                                     writer.addDocument(document);
435                                     didReplace |= true;
436                                     if (IndexPolicy.TRACE_INDEX_UPDATE)
437                                         System.out.println("-replaced single existing");
438                                 } else {
439                                     if (IndexPolicy.TRACE_INDEX_UPDATE)
440                                         System.out.println("-was actually same than single existing");
441                                 }
442                                 done = true;
443                             } 
444                     }
445                     if(!done) {
446                         writer.deleteDocuments(removedTerm);
447                         writer.addDocument(document);
448                         didReplace |= true;
449                         if (IndexPolicy.TRACE_INDEX_UPDATE)
450                                 System.out.println("-had many or none - removed all existing");
451                     }
452                     
453                 }
454
455             if (IndexPolicy.PERF_INDEX_UPDATE) {
456                 end = System.nanoTime();
457                 double ms = (end - start) * 1e-6;
458                 System.out.println(getDescriptor() + "Replaced " + keyValues.size() + " documents from index at " + indexPath + " in " + ms + " ms");
459             }
460
461         } finally {
462         }
463         
464         return didReplace;
465         
466     }
467     
468     protected boolean requireChangeInfoOnReplace() {
469         return true;
470     }
471     
472     private boolean areSame(Document d1, Document d2) {
473         List<IndexableField> fs1 = d1.getFields();
474         List<IndexableField> fs2 = d2.getFields();
475         if(fs1.size() != fs2.size()) return false;
476         for(int i=0;i<fs1.size();i++) {
477                 IndexableField f1 = fs1.get(i);
478                 IndexableField f2 = fs2.get(i);
479                 String s1 = f1.stringValue();
480                 String s2 = f2.stringValue();
481             if (IndexPolicy.TRACE_INDEX_UPDATE)
482                 System.err.println("areSame " + s1 + " vs. " + s2 );
483                 if(!ObjectUtils.objectEquals(s1,s2)) return false;
484         }
485         return true;
486     }
487
488     final RequestProcessor session;
489
490     final Resource         relation;
491
492     /**
493      * The schema of the index, i.e. the fields that will be indexed per
494      * document for the specified relation. Since the relation stays the same
495      * throughout the lifetime of this class, the index schema is also assumed
496      * to the same. This means that {@link GenericRelation#getFields()} is
497      * assumed to stay the same.
498      */
499     final IndexSchema      schema;
500
501     Resource         input;
502
503     Path             indexPath;
504
505     Directory        directory;
506
507     IndexReader      reader;
508
509     IndexWriter      writer;
510
511     IndexSearcher    searcher;
512
513     IndexedRelationsSearcherBase(RequestProcessor session, Resource relation, Resource input) {
514         this.session = session;
515         this.relation = relation;
516         this.input = input;
517         this.indexPath = getIndexDirectory(session.getSession(), relation, input);
518         if(isIndexAvailable()) {
519                 state = State.READY;
520         } else {
521                 state = State.NONE;
522         }
523         this.schema = IndexSchema.readFromRelation(session, relation);
524     }
525
526     Directory getDirectory(Session session) throws IOException {
527         return FSDirectory.open(indexPath.toFile());
528     }
529
530     abstract String getDescriptor();
531     
532     /**
533      * Ensures that searcher is in read or write state.
534      * 
535      * @param forWriting <code>true</code> to open index for writing,
536      *        <code>false</code> for reading
537      * @return true is required state was reached       
538      *        
539      */
540     boolean startAccess(IProgressMonitor monitor, Session session, boolean forWriting) {
541         if(forWriting) {
542                 changeState(monitor, session, State.WRITE);
543                 return checkState(State.WRITE);
544         } else {
545                 changeState(monitor, session, State.READ);
546                 return checkState(State.READ);
547         }
548     }
549
550     boolean hasAccess(boolean forWriting) {
551         
552         if (forWriting)
553                 return checkState(State.WRITE); 
554         else
555                 return checkState(State.WRITE) || checkState(State.READ);
556         
557     }
558     
559     void assertAccessOpen(boolean forWriting) {
560         if (forWriting)
561                 if(!checkState(State.WRITE)) 
562                 throw new IllegalStateException("index not opened for writing (directory=" + directory + ", reader=" + reader + ")");
563         else
564                 if(!(checkState(State.WRITE) || checkState(State.READ))) 
565                 throw new IllegalStateException("index not opened for reading (directory=" + directory + ", writer=" + writer + ")");
566     }
567     
568     void closeWriter(IndexWriter writer) throws CorruptIndexException, IOException {
569         if (writer == null)
570             return;
571
572         try {
573             // May throw OOME, see IndexWriter javadoc for the correct actions.
574             writer.close(false);
575         } catch (OutOfMemoryError e) {
576             writer.close();
577             throw e;
578         }
579     }
580
581     public static String getPattern(GenericRelation relation, int boundCount) {
582         String result = "";
583         for (int i = 0; i < boundCount; i++)
584             result += "b";
585         for (int i = 0; i < relation.getFields().length - boundCount; i++)
586             result += "f";
587         return result;
588     }
589
590     void initializeIndex(IProgressMonitor monitor, ReadGraph graph, Object[] bound, boolean overwrite)
591             throws IOException, DatabaseException
592     {
593         IndexingJob.jobifyIfPossible(
594                 monitor,
595                 "Reindexing " + NameUtils.getSafeLabel(graph, input),
596                 mon -> {
597                     try {
598                         GenericRelation r = graph.adapt(relation, GenericRelation.class);
599                         if (r == null)
600                             throw new DatabaseException("Given resource " + relation + "could not be adapted to GenericRelation.");
601
602                         GenericRelation selection = r.select(getPattern(r, bound.length), bound);
603
604                         List<Object[]> results = selection.realize(graph);
605                         initializeIndexImpl(new CompletableFuture<>(), mon, r, results, bound, overwrite);
606                     } catch (IOException e) {
607                         getLogger().error("Index is in problematic state! {}", this, e);
608                         throw new DatabaseException(e);
609                     }
610                 });
611     }
612
613     private static final int INDEXING_THREAD_COUNT = 2; // this is quite good parallelism level for lucene
614
615     void initializeIndexImpl(CompletableFuture<?> result, IProgressMonitor monitor, GenericRelation r, List<Object[]> results, final Object[] bound, boolean overwrite) throws IOException {
616         try {
617             final SubMonitor mon = SubMonitor.convert(monitor, 100);
618     
619             if (IndexPolicy.TRACE_INDEX_INIT)
620                 System.out.println(getDescriptor() + "Initializing index at " + indexPath + " (overwrite = " + overwrite + ")");
621             mon.beginTask("Initializing Index", 100);
622     
623             if (overwrite) {
624                 if (Files.exists(indexPath)) {
625                     mon.subTask("Erasing previous index");
626                     if (getLogger().isDebugEnabled())
627                         getLogger().debug("Erasing previous index {}", indexPath.toAbsolutePath());
628                     FileUtils.emptyDirectory(indexPath);
629                 }
630             }
631     
632             final AtomicReference<FSDirectory> directory = new AtomicReference<FSDirectory>();
633             final AtomicReference<IndexWriter> writer = new AtomicReference<IndexWriter>();
634     
635             try {
636                 mon.subTask("Start index write");
637                 Files.createDirectories(indexPath);
638     
639                 directory.set(FSDirectory.open(indexPath.toFile()));
640                 IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_4_9, Queries.getAnalyzer()).setOpenMode(OpenMode.CREATE);
641                 writer.set(new IndexWriter(directory.get(), conf));
642     
643                 mon.worked(5);
644     
645                 long realizeStart = 0;
646                 if (IndexPolicy.PERF_INDEX_INIT)
647                     realizeStart = System.nanoTime();
648     
649                 mon.subTask("Calculating indexed content");
650                 mon.worked(5);
651                 
652                 mon.worked(40);
653     
654                 if (IndexPolicy.PERF_INDEX_INIT)
655                     System.out.println(getDescriptor() + "Realized index with " + results.size() + " entries at " + indexPath + " in " + (1e-9 * (System.nanoTime()-realizeStart)) + " seconds.");
656     
657                 if (IndexPolicy.TRACE_INDEX_INIT)
658                     System.out.println(getDescriptor() + "Indexed relation " + r + " produced " + results.size() + " results");
659                 
660                 long start = IndexPolicy.PERF_INDEX_INIT ? System.nanoTime() : 0;
661     
662                 mon.subTask("Indexing content");
663                 final Semaphore s = new Semaphore(0);
664                 mon.setWorkRemaining(results.size());
665                 for (int i = 0; i < INDEXING_THREAD_COUNT; i++) {
666                     final int startIndex = i;
667                     ThreadUtils.getBlockingWorkExecutor().submit(() -> {
668                         try {
669                             Document document = new Document();
670                             Field[] fs = makeFieldsForRelation(r, bound.length, document);
671     
672                             for (int index = startIndex; index < results.size(); index += INDEXING_THREAD_COUNT) {
673                                 if (setFields(fs, results.get(index)) == null)
674                                     continue;
675                                 try {
676                                     writer.get().addDocument(document);
677                                 } catch (CorruptIndexException e) {
678                                     getLogger().error("Index is corrupted! {}", this, e);
679                                     throw new IllegalStateException(e);
680                                 } catch (IOException e) {
681                                     getLogger().error("Index is in problematic state! {}", this, e);
682                                     throw new IllegalStateException(e);
683                                 } finally {
684                                     synchronized (mon) {
685                                         mon.worked(1);
686                                     }
687                                 }
688                             }
689                         } catch (DatabaseException e) {
690                             throw new IllegalStateException(e);
691                         } finally {
692                             s.release();
693                         }
694                     });
695                 }
696     
697                 try {
698                     s.acquire(INDEXING_THREAD_COUNT);
699                 } catch (InterruptedException e) {
700                     getLogger().error("Could not initialize index {}", this, e);
701                 }
702     
703                 // http://www.gossamer-threads.com/lists/lucene/java-dev/47895
704                 // and http://lucene.apache.org/java/docs/index.html#27+November+2011+-+Lucene+Core+3.5.0
705                 // advise against calling optimize at all. So let's not do it anymore.
706                 //writer.get().optimize();
707                 //writer.get().commit();
708     
709                 mon.subTask("Flushing");
710     
711                 if (IndexPolicy.PERF_INDEX_INIT)
712                     System.out.println(getDescriptor() + "Wrote index at " + indexPath + " in " + (1e-9 * (System.nanoTime()-start)) + " seconds.");
713                 
714                 result.complete(null);
715     //        } catch (DatabaseException e) {
716     //            getLogger().error("Could not initialize index due to db {}", this, e);
717             } finally {
718                 try {
719                     closeWriter(writer.getAndSet(null));
720                 } finally {
721                     FileUtils.uncheckedClose(directory.getAndSet(null));
722                 }
723             }
724         } catch (Throwable t) {
725             getLogger().error("Could not initialize index", t);
726             result.completeExceptionally(t);
727         }
728     }
729
730     
731     public List<Object[]> debugDocs(IProgressMonitor monitor) throws ParseException, IOException, DatabaseException {
732     
733             Query query = new MatchAllDocsQuery(); 
734         
735             TopDocs td = searcher.search(query, Integer.MAX_VALUE);
736     
737             ScoreDoc[ ] scoreDocs = td.scoreDocs; 
738             List<Object[]> result = new ArrayList<Object[]>(scoreDocs.length);
739         
740             for(ScoreDoc scoreDoc:scoreDocs) {
741         
742                 try {
743         
744                     Document doc = reader.document(scoreDoc.doc);
745                     List<IndexableField> fs = doc.getFields();
746                     Object[] o = new Object[fs.size()];
747                     int index = 0; 
748                     for (IndexableField f : fs) {
749                     o[index++] = f.stringValue();
750                     }
751                     result.add(o);
752         
753             } catch (CorruptIndexException e) {
754                 getLogger().error("Index is corrupted! {}", this, e);
755                 throw new DatabaseException(e);
756             } catch (IOException e) {
757                 getLogger().error("Index is in problematic state! {}", this, e);
758                 throw new DatabaseException(e);
759             }
760
761             }
762             
763             return result;
764             
765     }
766
767     
768     List<Map<String, Object>> doSearch(IProgressMonitor monitor, RequestProcessor processor, String search, int maxResultCount) throws ParseException, IOException,
769     DatabaseException {
770
771         // An empty search string will crash QueryParser
772         // Just return no results for empty queries.
773         //System.out.println("search: '" + search + "'");
774         if (search.isEmpty())
775             return Collections.emptyList();
776
777         assertAccessOpen(false);
778
779         Query query = Queries.parse(search, schema);
780
781         long start = System.nanoTime();
782
783         maxResultCount = Math.min(maxResultCount, searcher.getIndexReader().numDocs());
784         if (maxResultCount == 0)
785             return Collections.emptyList();
786         
787         final TopDocs docs = searcher.search(query, null, maxResultCount);
788         
789 //        for(Object[] o : debugDocs(monitor)) {
790 //            System.err.println("-" + Arrays.toString(o));
791 //        }
792         
793         if (IndexPolicy.PERF_INDEX_QUERY) {
794             long end = System.nanoTime();
795             System.out.println(getDescriptor() + "search(" + search + ", " + maxResultCount + ") into index at " + indexPath + " took " + (1e-9 * (end-start)) + " seconds.");
796         }
797
798         if (docs.totalHits == 0) {
799             return Collections.emptyList();
800         }
801
802         return processor.syncRequest(new Read<List<Map<String, Object>>>() {
803
804             @Override
805             public List<Map<String, Object>> perform(ReadGraph graph) throws DatabaseException {
806
807                 GenericRelation r = graph.adapt(relation, GenericRelation.class);
808                 if (r == null)
809                     throw new DatabaseException("Given resource " + graph.syncRequest(new SafeName(relation))
810                             + "could not be adapted to GenericRelation.");
811
812                 SerialisationSupport support = graph.getService(SerialisationSupport.class);
813
814                 List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(docs.scoreDocs.length);
815                 
816                 final DocumentStoredFieldVisitor visitor = new DocumentStoredFieldVisitor();
817                 
818                 for (ScoreDoc scoreDoc : docs.scoreDocs) {
819
820                     try {
821
822                         reader.document(scoreDoc.doc, visitor);
823                         
824                         Document doc = visitor.getDocument();
825
826                         List<IndexableField> fs = doc.getFields();
827                         Map<String, Object> entry = new THashMap<String, Object>(fs.size());
828                         for (IndexableField f : fs) {
829                             IndexSchema.Type type = schema.typeMap.get(f.name());
830                             if (type == IndexSchema.Type.LONG) {
831                                 entry.put(f.name(), support.getResource(f.numericValue().longValue()));
832                             } else {
833                                 entry.put(f.name(), f.stringValue());
834                             }
835                         }
836                         
837                         result.add(entry);
838
839                     } catch (CorruptIndexException e) {
840                         getLogger().error("Index is corrupted! {}", this, e);
841                         throw new DatabaseException(e);
842                     } catch (IOException e) {
843                         getLogger().error("Index is in problematic state! {}", this, e);
844                         throw new DatabaseException(e);
845                     }
846                 }
847                 return result;
848             }
849         });
850     }
851
852     static class ResourceVisitor extends StoredFieldVisitor {
853         
854         public long id;
855
856                 @Override
857                 public Status needsField(FieldInfo fieldInfo) throws IOException {
858                         if("Resource".equals(fieldInfo.name)) return Status.YES;
859                         return Status.NO;
860                 }
861                 
862                 @Override
863                 public void longField(FieldInfo fieldInfo, long value) throws IOException {
864                         id = value;
865                 }
866         
867     };
868     
869     static class DumpVisitor extends StoredFieldVisitor {
870
871         public List<Object> values;
872         
873         DumpVisitor(List<Object> values) {
874                 this.values = values;
875         }
876
877                 @Override
878                 public Status needsField(FieldInfo fieldInfo) throws IOException {
879                         return Status.YES;
880                 }
881                 
882                 @Override
883                 public void longField(FieldInfo fieldInfo, long value) throws IOException {
884                         values.add(value);
885                 }
886                 
887                 @Override
888                 public void stringField(FieldInfo fieldInfo, String value) throws IOException {
889                         values.add(value);
890                 }
891
892     }
893
894     List<Resource> doSearchResources(IProgressMonitor monitor, RequestProcessor processor, String search, int maxResultCount) throws ParseException, IOException,
895     DatabaseException {
896
897         // An empty search string will crash QueryParser
898         // Just return no results for empty queries.
899         //System.out.println("search: '" + search + "'");
900         if (search.isEmpty())
901             return Collections.emptyList();
902
903         assertAccessOpen(false);
904
905         Query query = Queries.parse(search, schema);
906
907         long start = System.nanoTime();
908
909         maxResultCount = Math.min(maxResultCount, searcher.getIndexReader().numDocs());
910         if (maxResultCount == 0)
911             return Collections.emptyList();
912         
913         final TopDocs docs = searcher.search(query, null, maxResultCount);
914         
915 //        for(Object[] o : debugDocs(monitor)) {
916 //            System.err.println("-" + Arrays.toString(o));
917 //        }
918         
919         if (IndexPolicy.PERF_INDEX_QUERY) {
920             long end = System.nanoTime();
921             System.out.println(getDescriptor() + "search(" + search + ", " + maxResultCount + ") into index at " + indexPath + " took " + (1e-9 * (end-start)) + " seconds.");
922         }
923
924         if (docs.totalHits == 0) {
925             return Collections.emptyList();
926         }
927         
928         return processor.syncRequest(new Read<List<Resource>>() {
929
930             @Override
931             public List<Resource> perform(ReadGraph graph) throws DatabaseException {
932
933                 CollectionSupport cs = graph.getService(CollectionSupport.class);
934                 SerialisationSupport support = graph.getService(SerialisationSupport.class);
935                 
936                 List<Resource> result = cs.createList();
937                 
938                 ResourceVisitor visitor = new ResourceVisitor();
939                 
940                 for (ScoreDoc scoreDoc : docs.scoreDocs) {
941                     try {
942                         reader.document(scoreDoc.doc, visitor);
943                         result.add(support.getResource(visitor.id));
944                     } catch (CorruptIndexException e) {
945                         getLogger().error("Index is corrupted! {}", this, e);
946                         throw new DatabaseException(e);
947                     } catch (IOException e) {
948                         getLogger().error("Index is in problematic state! {}", this, e);
949                         throw new DatabaseException(e);
950                     }
951                 }
952                 return result;
953             }
954         });
955     }
956
957     List<Object> doList(IProgressMonitor monitor, RequestProcessor processor) throws ParseException, IOException,
958     DatabaseException {
959
960         assertAccessOpen(false);
961
962         Query query = new MatchAllDocsQuery(); 
963
964         final TopDocs docs = searcher.search(query, Integer.MAX_VALUE);
965         
966         ArrayList<Object> result = new ArrayList<Object>();
967         
968         DumpVisitor visitor = new DumpVisitor(result);
969                 
970         for (ScoreDoc scoreDoc : docs.scoreDocs) {
971
972                 try {
973
974                         reader.document(scoreDoc.doc, visitor);
975
976                 } catch (CorruptIndexException e) {
977                     getLogger().error("Index is corrupted! {}", this, e);
978                         throw new DatabaseException(e);
979                 } catch (IOException e) {
980                     getLogger().error("Index is in problematic state! {}", this, e);
981                         throw new DatabaseException(e);
982                 }
983
984         }
985
986         return result;
987
988     }
989     
990     protected static Path getIndexDirectory(Session session, Resource relation, Resource input) {
991         Path path = DatabaseIndexing.getIndexLocation(session, relation, input);
992 //        System.out.println("getIndexDirectory = " + path);
993         return path;
994     }
995
996     Path getIndexPath() {
997         return indexPath;
998     }
999
1000     boolean isIndexAvailable() {
1001         return Files.isDirectory(indexPath);
1002     }
1003
1004     abstract Throwable bestEffortClear(IProgressMonitor monitor, Session session);
1005
1006     /*
1007      * Start from scratch. Clear all caches and rebuild the index. 
1008      */
1009     Throwable clearDirectory(IProgressMonitor monitor, Session session) {
1010         
1011                 Path file = getIndexPath();
1012
1013         try {
1014                         FileUtils.delete(file);
1015         } catch (Throwable t) {
1016                 getLogger().error("Could not delete directory {}", file.toAbsolutePath(), t);
1017                 return t;
1018         }
1019         if (Files.exists(file))
1020             return new IllegalStateException("Failed to delete directory " + file.toAbsolutePath());
1021         return null;
1022     }
1023
1024     private Field[] setFields(Field[] fs, Object[] result) {
1025         for (int i = 0; i < result.length; i++) {
1026             Object value = result[i];
1027             if (value instanceof String) {
1028                 if (IndexPolicy.DEBUG_INDEX_INIT)
1029                     System.out.println(getDescriptor() + "index " + fs[i].name() + " = " + value + " : String");
1030                 fs[i].setStringValue((String) value);
1031             } else if (value instanceof Long) {
1032                 if (IndexPolicy.DEBUG_INDEX_INIT)
1033                     System.out.println(getDescriptor() + "index " + fs[i].name() + " = " + value + " : Long");
1034                 fs[i].setLongValue((Long) value);
1035             } else {
1036                 getLogger().error("Can only index Long and String fields, encountered " + value);
1037                 return null;
1038             }
1039         }
1040         return fs;
1041     }
1042
1043     protected abstract Logger getLogger();
1044     
1045     @Override
1046     public String toString() {
1047         return getClass().getSimpleName() + " [" + String.valueOf(schema) + ", " + String.valueOf(relation) + ", " + String.valueOf(input) + ", " + String.valueOf(indexPath) + ", " + String.valueOf(directory) + ", " + String.valueOf(state) + "]";
1048     }
1049 }