1 /*******************************************************************************
2 * Copyright (c) 2014, 2015 Association for Decentralized Information Management
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
10 * Semantum Oy - initial API and implementation
11 * Semantum Oy - improvements for Simantics issue #6053
12 *******************************************************************************/
13 package org.simantics.db.indexing;
15 import java.io.Reader;
16 import java.util.HashMap;
18 import java.util.concurrent.atomic.AtomicReference;
19 import java.util.regex.Pattern;
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;
39 public class Queries {
41 static final class LowerCaseWhitespaceTokenizer extends CharTokenizer {
43 * Construct a new WhitespaceTokenizer. * @param matchVersion Lucene version
44 * to match See {@link <a href="#version">above</a>}
47 * the input to split up into tokens
49 public LowerCaseWhitespaceTokenizer(Version matchVersion, Reader in) {
50 super(matchVersion, in);
54 * Construct a new WhitespaceTokenizer using a given
55 * {@link org.apache.lucene.util.AttributeSource.AttributeFactory}.
58 * matchVersion Lucene version to match See
59 * {@link <a href="#version">above</a>}
61 * the attribute factory to use for this {@link Tokenizer}
63 * the input to split up into tokens
65 public LowerCaseWhitespaceTokenizer(Version matchVersion, AttributeFactory factory, Reader in) {
66 super(matchVersion, factory, in);
70 protected int normalize(int c) {
71 return Character.toLowerCase(c);
74 protected boolean isTokenChar(int c) {
75 return !Character.isWhitespace(c);
79 static final class LowerCaseWhitespaceAnalyzer extends Analyzer {
81 private final Version matchVersion;
84 * Creates a new {@link WhitespaceAnalyzer}
85 * @param matchVersion Lucene version to match See {@link <a href="#version">above</a>}
87 public LowerCaseWhitespaceAnalyzer(Version matchVersion) {
88 this.matchVersion = matchVersion;
92 protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {
93 return new TokenStreamComponents(new LowerCaseWhitespaceTokenizer(matchVersion, reader));
97 static final class TypeStringAnalyzer extends Analyzer {
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);
104 return new TokenStreamComponents(tokenizer, filter);
109 private static AtomicReference<Pair<Query, String>> queryCache = new AtomicReference<>();
111 final static PerFieldAnalyzerWrapper analyzer = createAnalyzer();
113 static PerFieldAnalyzerWrapper createAnalyzer() {
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());
123 PerFieldAnalyzerWrapper analyzer = new PerFieldAnalyzerWrapper(new LowerCaseWhitespaceAnalyzer(Version.LUCENE_4_9), analyzerPerField);
128 static PerFieldAnalyzerWrapper getAnalyzer() {
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;
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);
142 queryCache.set(Pair.make(query, search));
147 public static class CustomQueryParser extends QueryParser {
149 protected final IndexSchema schema;
151 public CustomQueryParser(Version version, String field, Analyzer analyzer, IndexSchema schema) {
152 super(version, field, analyzer);
153 this.schema = schema;
154 setAllowLeadingWildcard(true);
158 protected Query getRangeQuery(
162 boolean startInclusive,
163 boolean endInclusive) throws ParseException
165 IndexSchema.Type type = schema.typeMap.get(field);
166 if (IndexSchema.NUMERIC_TYPES.contains(type)) {
167 boolean equalParts = ObjectUtils.objectEquals(part1, part2);
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);
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);
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);
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);
191 throw new ParseException("Unrecognized numeric field type '" + type + "' for field '" + field + "'");
193 } catch (NumberFormatException e) {
194 throw new ParseException(e.getMessage());
197 return super.getRangeQuery(field, part1, part2, startInclusive, endInclusive);