]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.db.indexing/src/org/simantics/db/indexing/Queries.java
Improvements to Lucene indexing
[simantics/platform.git] / bundles / org.simantics.db.indexing / src / org / simantics / db / indexing / Queries.java
1 /*******************************************************************************
2  * Copyright (c) 2014, 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  *     Semantum Oy - initial API and implementation
11  *     Semantum Oy - improvements for Simantics issue #6053
12  *******************************************************************************/
13 package org.simantics.db.indexing;
14
15 import java.io.Reader;
16 import java.util.HashMap;
17 import java.util.Map;
18 import java.util.concurrent.atomic.AtomicReference;
19 import java.util.regex.Pattern;
20
21 import org.apache.lucene.analysis.Analyzer;
22 import org.apache.lucene.analysis.TokenFilter;
23 import org.apache.lucene.analysis.Tokenizer;
24 import org.apache.lucene.analysis.core.KeywordAnalyzer;
25 import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
26 import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;
27 import org.apache.lucene.analysis.pattern.PatternReplaceFilter;
28 import org.apache.lucene.analysis.pattern.PatternTokenizer;
29 import org.apache.lucene.analysis.util.CharTokenizer;
30 import org.apache.lucene.queryparser.classic.ParseException;
31 import org.apache.lucene.queryparser.classic.QueryParser;
32 import org.apache.lucene.search.NumericRangeQuery;
33 import org.apache.lucene.search.Query;
34 import org.apache.lucene.util.AttributeFactory;
35 import org.apache.lucene.util.Version;
36 import org.simantics.databoard.util.ObjectUtils;
37 import org.simantics.utils.datastructures.Pair;
38
39 public class Queries {
40
41     static final class LowerCaseWhitespaceTokenizer extends CharTokenizer {
42         /**
43          * Construct a new WhitespaceTokenizer. * @param matchVersion Lucene version
44          * to match See {@link <a href="#version">above</a>}
45          * 
46          * @param in
47          *          the input to split up into tokens
48          */
49         public LowerCaseWhitespaceTokenizer(Version matchVersion, Reader in) {
50             super(matchVersion, in);
51         }
52
53         /**
54          * Construct a new WhitespaceTokenizer using a given
55          * {@link org.apache.lucene.util.AttributeSource.AttributeFactory}.
56          *
57          * @param
58          *          matchVersion Lucene version to match See
59          *          {@link <a href="#version">above</a>}
60          * @param factory
61          *          the attribute factory to use for this {@link Tokenizer}
62          * @param in
63          *          the input to split up into tokens
64          */
65         public LowerCaseWhitespaceTokenizer(Version matchVersion, AttributeFactory factory, Reader in) {
66             super(matchVersion, factory, in);
67         }
68
69         @Override
70         protected int normalize(int c) {
71             return Character.toLowerCase(c);
72         }
73
74         protected boolean isTokenChar(int c) {
75             return !Character.isWhitespace(c);
76         }
77     }
78
79     static final class LowerCaseWhitespaceAnalyzer extends Analyzer {
80
81         private final Version matchVersion;
82
83         /**
84          * Creates a new {@link WhitespaceAnalyzer}
85          * @param matchVersion Lucene version to match See {@link <a href="#version">above</a>}
86          */
87         public LowerCaseWhitespaceAnalyzer(Version matchVersion) {
88             this.matchVersion = matchVersion;
89         }
90
91         @Override
92         protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {
93             return new TokenStreamComponents(new LowerCaseWhitespaceTokenizer(matchVersion, reader));
94         }
95     }
96
97     static final class TypeStringAnalyzer extends Analyzer {
98
99                 @Override
100                 protected TokenStreamComponents createComponents(String fieldName, Reader reader) {
101                         Tokenizer tokenizer = new PatternTokenizer(reader, Pattern.compile("(([^\\\\ ]|\\\\\\\\|\\\\ )+)( *)"), 1);
102                         TokenFilter filter = new PatternReplaceFilter(tokenizer, Pattern.compile("(\\\\(\\\\| ))"), "$2", true);
103                         
104                         return new TokenStreamComponents(tokenizer, filter);
105                 }
106                 
107         }
108
109     private static AtomicReference<Pair<Query, String>> queryCache = new AtomicReference<>();
110
111     final static PerFieldAnalyzerWrapper analyzer = createAnalyzer();
112     
113     static PerFieldAnalyzerWrapper createAnalyzer() {
114         
115         Map<String,Analyzer> analyzerPerField = new HashMap<>();
116         analyzerPerField.put("Model", new KeywordAnalyzer());
117         analyzerPerField.put("Parent", new KeywordAnalyzer());
118         analyzerPerField.put("Resource", new KeywordAnalyzer());
119         analyzerPerField.put("GUID", new KeywordAnalyzer());
120         analyzerPerField.put("Name", new KeywordAnalyzer());
121         analyzerPerField.put("Types", new TypeStringAnalyzer());
122         
123         PerFieldAnalyzerWrapper analyzer = new PerFieldAnalyzerWrapper(new LowerCaseWhitespaceAnalyzer(Version.LUCENE_4_9), analyzerPerField);
124         return analyzer;
125         
126     }
127
128     static PerFieldAnalyzerWrapper getAnalyzer() {
129         return analyzer;
130     }
131
132     static Query parse(String search, IndexSchema schema) throws ParseException {
133         Pair<Query, String> cachedQuery = queryCache.get();
134         if (cachedQuery != null && search.equals(cachedQuery.second))
135             return cachedQuery.first;
136
137         //System.err.println("parse " + search + " (cached=" + (cachedQuery != null ? cachedQuery.second : "null") + ")" );
138         CustomQueryParser parser = new CustomQueryParser(Version.LUCENE_4_9, "Name", getAnalyzer(), schema);
139         parser.setLowercaseExpandedTerms(false);
140         Query query = parser.parse(search);
141
142         queryCache.set(Pair.make(query, search));
143         return query;
144     }
145
146
147     public static class CustomQueryParser extends QueryParser {
148
149         protected final IndexSchema schema;
150
151         public CustomQueryParser(Version version, String field, Analyzer analyzer, IndexSchema schema) {
152             super(version, field, analyzer);
153             this.schema = schema;
154             setAllowLeadingWildcard(true);
155         }
156
157         @Override
158         protected Query getRangeQuery(
159                 String field,
160                 String part1,
161                 String part2,
162                 boolean startInclusive,
163                 boolean endInclusive) throws ParseException
164         {
165             IndexSchema.Type type = schema.typeMap.get(field);
166             if (IndexSchema.NUMERIC_TYPES.contains(type)) {
167                 boolean equalParts = ObjectUtils.objectEquals(part1, part2);
168                 try {
169                     switch (type) {
170                     case INT: {
171                         Integer min = part1 != null ? (                   Integer.valueOf(part1)) : null;
172                         Integer max = part2 != null ? (equalParts ? min : Integer.valueOf(part2)) : null;
173                         return NumericRangeQuery.newIntRange(field, min, max, startInclusive, endInclusive);
174                     }
175                     case LONG: {
176                         Long min = part1 != null ? (                   Long.valueOf(part1)) : null;
177                         Long max = part2 != null ? (equalParts ? min : Long.valueOf(part2)) : null;
178                         return NumericRangeQuery.newLongRange(field, min, max, startInclusive, endInclusive);
179                     }
180                     case FLOAT: {
181                         Float min = part1 != null ? (                   Float.valueOf(part1)) : null;
182                         Float max = part2 != null ? (equalParts ? min : Float.valueOf(part2)) : null;
183                         return NumericRangeQuery.newFloatRange(field, min, max, startInclusive, endInclusive);
184                     }
185                     case DOUBLE: {
186                         Double min = part1 != null ? (                   Double.valueOf(part1)) : null;
187                         Double max = part2 != null ? (equalParts ? min : Double.valueOf(part2)) : null;
188                         return NumericRangeQuery.newDoubleRange(field, min, max, startInclusive, endInclusive);
189                     }
190                     default:
191                         throw new ParseException("Unrecognized numeric field type '" + type + "' for field '" + field + "'"); 
192                     }
193                 } catch (NumberFormatException e) {
194                     throw new ParseException(e.getMessage()); 
195                 }
196             }
197             return super.getRangeQuery(field, part1, part2, startInclusive, endInclusive); 
198         } 
199
200     }
201
202 }