]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.db.indexing/src/org/simantics/db/indexing/Queries.java
Index tokenized lowercase versions of name and types for UI searches
[simantics/platform.git] / bundles / org.simantics.db.indexing / src / org / simantics / db / indexing / Queries.java
index 5d071d323e590daa5a9df64874467c897d93dfc2..21b32fa1ecd14d1ae0a5238095365d4dbd56f6ff 100644 (file)
-/*******************************************************************************\r
- * Copyright (c) 2014, 2015 Association for Decentralized Information Management\r
- * in Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- *     Semantum Oy - initial API and implementation\r
- *     Semantum Oy - improvements for Simantics issue #6053\r
- *******************************************************************************/\r
-package org.simantics.db.indexing;\r
-\r
-import java.io.Reader;\r
-import java.util.HashMap;\r
-import java.util.Map;\r
-import java.util.concurrent.atomic.AtomicReference;\r
-\r
-import org.apache.lucene.analysis.Analyzer;\r
-import org.apache.lucene.analysis.Tokenizer;\r
-import org.apache.lucene.analysis.core.KeywordAnalyzer;\r
-import org.apache.lucene.analysis.core.WhitespaceAnalyzer;\r
-import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;\r
-import org.apache.lucene.analysis.util.CharTokenizer;\r
-import org.apache.lucene.queryparser.classic.ParseException;\r
-import org.apache.lucene.queryparser.classic.QueryParser;\r
-import org.apache.lucene.search.NumericRangeQuery;\r
-import org.apache.lucene.search.Query;\r
-import org.apache.lucene.util.AttributeFactory;\r
-import org.apache.lucene.util.Version;\r
-import org.simantics.databoard.util.ObjectUtils;\r
-import org.simantics.utils.datastructures.Pair;\r
-\r
-public class Queries {\r
-\r
-    static final class LowerCaseWhitespaceTokenizer extends CharTokenizer {\r
-        /**\r
-         * Construct a new WhitespaceTokenizer. * @param matchVersion Lucene version\r
-         * to match See {@link <a href="#version">above</a>}\r
-         * \r
-         * @param in\r
-         *          the input to split up into tokens\r
-         */\r
-        public LowerCaseWhitespaceTokenizer(Version matchVersion, Reader in) {\r
-            super(matchVersion, in);\r
-        }\r
-\r
-        /**\r
-         * Construct a new WhitespaceTokenizer using a given\r
-         * {@link org.apache.lucene.util.AttributeSource.AttributeFactory}.\r
-         *\r
-         * @param\r
-         *          matchVersion Lucene version to match See\r
-         *          {@link <a href="#version">above</a>}\r
-         * @param factory\r
-         *          the attribute factory to use for this {@link Tokenizer}\r
-         * @param in\r
-         *          the input to split up into tokens\r
-         */\r
-        public LowerCaseWhitespaceTokenizer(Version matchVersion, AttributeFactory factory, Reader in) {\r
-            super(matchVersion, factory, in);\r
-        }\r
-\r
-        @Override\r
-        protected int normalize(int c) {\r
-            return Character.toLowerCase(c);\r
-        }\r
-\r
-        protected boolean isTokenChar(int c) {\r
-            return !Character.isWhitespace(c);\r
-        }\r
-    }\r
-\r
-    static final class LowerCaseWhitespaceAnalyzer extends Analyzer {\r
-\r
-        private final Version matchVersion;\r
-\r
-        /**\r
-         * Creates a new {@link WhitespaceAnalyzer}\r
-         * @param matchVersion Lucene version to match See {@link <a href="#version">above</a>}\r
-         */\r
-        public LowerCaseWhitespaceAnalyzer(Version matchVersion) {\r
-            this.matchVersion = matchVersion;\r
-        }\r
-\r
-        @Override\r
-        protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {\r
-            return new TokenStreamComponents(new LowerCaseWhitespaceTokenizer(matchVersion, reader));\r
-        }\r
-    }\r
-\r
-    private static AtomicReference<Pair<Query, String>> queryCache = new AtomicReference<>();\r
-\r
-    final static PerFieldAnalyzerWrapper analyzer = createAnalyzer();\r
-    \r
-    static PerFieldAnalyzerWrapper createAnalyzer() {\r
-       \r
-       Map<String,Analyzer> analyzerPerField = new HashMap<>();\r
-       analyzerPerField.put("Model", new KeywordAnalyzer());\r
-       analyzerPerField.put("Parent", new KeywordAnalyzer());\r
-       analyzerPerField.put("Resource", new KeywordAnalyzer());\r
-       analyzerPerField.put("GUID", new KeywordAnalyzer());\r
-       \r
-        PerFieldAnalyzerWrapper analyzer = new PerFieldAnalyzerWrapper(new LowerCaseWhitespaceAnalyzer(Version.LUCENE_4_9), analyzerPerField);\r
-        return analyzer;\r
-        \r
-    }\r
-\r
-    static PerFieldAnalyzerWrapper getAnalyzer() {\r
-       return analyzer;\r
-    }\r
-\r
-    static Query parse(String search, IndexSchema schema) throws ParseException {\r
-        Pair<Query, String> cachedQuery = queryCache.get();\r
-        if (cachedQuery != null && search.equals(cachedQuery.second))\r
-            return cachedQuery.first;\r
-\r
-        //System.err.println("parse " + search + " (cached=" + (cachedQuery != null ? cachedQuery.second : "null") + ")" );\r
-        CustomQueryParser parser = new CustomQueryParser(Version.LUCENE_4_9, "Name", getAnalyzer(), schema);\r
-        Query query = parser.parse(search);\r
-\r
-        queryCache.set(Pair.make(query, search));\r
-        return query;\r
-    }\r
-\r
-\r
-    public static class CustomQueryParser extends QueryParser {\r
-\r
-        protected final IndexSchema schema;\r
-\r
-        public CustomQueryParser(Version version, String field, Analyzer analyzer, IndexSchema schema) {\r
-            super(version, field, analyzer);\r
-            this.schema = schema;\r
-            setAllowLeadingWildcard(true);\r
-        }\r
-\r
-        @Override\r
-        protected Query getRangeQuery(\r
-                String field,\r
-                String part1,\r
-                String part2,\r
-                boolean startInclusive,\r
-                boolean endInclusive) throws ParseException\r
-        {\r
-            IndexSchema.Type type = schema.typeMap.get(field);\r
-            if (IndexSchema.NUMERIC_TYPES.contains(type)) {\r
-                boolean equalParts = ObjectUtils.objectEquals(part1, part2);\r
-                try {\r
-                    switch (type) {\r
-                    case INT: {\r
-                        Integer min = part1 != null ? (                   Integer.valueOf(part1)) : null;\r
-                        Integer max = part2 != null ? (equalParts ? min : Integer.valueOf(part2)) : null;\r
-                        return NumericRangeQuery.newIntRange(field, min, max, startInclusive, endInclusive);\r
-                    }\r
-                    case LONG: {\r
-                        Long min = part1 != null ? (                   Long.valueOf(part1)) : null;\r
-                        Long max = part2 != null ? (equalParts ? min : Long.valueOf(part2)) : null;\r
-                        return NumericRangeQuery.newLongRange(field, min, max, startInclusive, endInclusive);\r
-                    }\r
-                    case FLOAT: {\r
-                        Float min = part1 != null ? (                   Float.valueOf(part1)) : null;\r
-                        Float max = part2 != null ? (equalParts ? min : Float.valueOf(part2)) : null;\r
-                        return NumericRangeQuery.newFloatRange(field, min, max, startInclusive, endInclusive);\r
-                    }\r
-                    case DOUBLE: {\r
-                        Double min = part1 != null ? (                   Double.valueOf(part1)) : null;\r
-                        Double max = part2 != null ? (equalParts ? min : Double.valueOf(part2)) : null;\r
-                        return NumericRangeQuery.newDoubleRange(field, min, max, startInclusive, endInclusive);\r
-                    }\r
-                    default:\r
-                        throw new ParseException("Unrecognized numeric field type '" + type + "' for field '" + field + "'"); \r
-                    }\r
-                } catch (NumberFormatException e) {\r
-                    throw new ParseException(e.getMessage()); \r
-                }\r
-            }\r
-            return super.getRangeQuery(field, part1, part2, startInclusive, endInclusive); \r
-        } \r
-\r
-    }\r
-\r
-}\r
+/*******************************************************************************
+ * Copyright (c) 2014, 2015 Association for Decentralized Information Management
+ * in Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Semantum Oy - initial API and implementation
+ *     Semantum Oy - improvements for Simantics issue #6053
+ *******************************************************************************/
+package org.simantics.db.indexing;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.Tokenizer;
+import org.apache.lucene.analysis.core.KeywordAnalyzer;
+import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
+import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;
+import org.apache.lucene.analysis.pattern.PatternReplaceFilter;
+import org.apache.lucene.analysis.pattern.PatternTokenizer;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.util.CharTokenizer;
+import org.apache.lucene.queryparser.classic.ParseException;
+import org.apache.lucene.queryparser.classic.QueryParser;
+import org.apache.lucene.search.NumericRangeQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.util.AttributeFactory;
+import org.apache.lucene.util.Version;
+import org.simantics.databoard.util.ObjectUtils;
+import org.simantics.db.layer0.genericrelation.Dependencies;
+import org.simantics.utils.datastructures.Pair;
+
+public class Queries {
+
+    static final class LowerCaseWhitespaceTokenizer extends CharTokenizer {
+        /**
+         * Construct a new WhitespaceTokenizer. * @param matchVersion Lucene version
+         * to match See {@link <a href="#version">above</a>}
+         * 
+         * @param in
+         *          the input to split up into tokens
+         */
+        public LowerCaseWhitespaceTokenizer(Version matchVersion, Reader in) {
+            super(matchVersion, in);
+        }
+
+        /**
+         * Construct a new WhitespaceTokenizer using a given
+         * {@link org.apache.lucene.util.AttributeSource.AttributeFactory}.
+         *
+         * @param
+         *          matchVersion Lucene version to match See
+         *          {@link <a href="#version">above</a>}
+         * @param factory
+         *          the attribute factory to use for this {@link Tokenizer}
+         * @param in
+         *          the input to split up into tokens
+         */
+        public LowerCaseWhitespaceTokenizer(Version matchVersion, AttributeFactory factory, Reader in) {
+            super(matchVersion, factory, in);
+        }
+
+        @Override
+        protected int normalize(int c) {
+            return Character.toLowerCase(c);
+        }
+
+        protected boolean isTokenChar(int c) {
+            return !Character.isWhitespace(c);
+        }
+    }
+
+    static final class LowerCaseWhitespaceAnalyzer extends Analyzer {
+
+        private final Version matchVersion;
+
+        /**
+         * Creates a new {@link WhitespaceAnalyzer}
+         * @param matchVersion Lucene version to match See {@link <a href="#version">above</a>}
+         */
+        public LowerCaseWhitespaceAnalyzer(Version matchVersion) {
+            this.matchVersion = matchVersion;
+        }
+
+        @Override
+        protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {
+            return new TokenStreamComponents(new LowerCaseWhitespaceTokenizer(matchVersion, reader));
+        }
+    }
+
+    final static class LowercaseFilter extends TokenFilter {
+        private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
+
+        public LowercaseFilter(TokenStream in) {
+            super(in);
+        }
+
+        @Override
+        public boolean incrementToken() throws IOException {
+            if (!input.incrementToken()) return false;
+            String lowercase = termAtt.toString().toLowerCase();
+            termAtt.setEmpty().append(lowercase);
+            return true;
+        }
+    }
+
+    static final class TypeStringAnalyzer extends Analyzer {
+        private boolean lowercase;
+
+        public TypeStringAnalyzer(Boolean lowercase) {
+            this.lowercase = lowercase;
+        }
+        
+        @Override
+        protected TokenStreamComponents createComponents(String fieldName, Reader reader) {
+                       Tokenizer tokenizer = new PatternTokenizer(reader, Pattern.compile("(([^\\\\ ]|\\\\\\\\|\\\\ )+)( *)"), 1);
+                       TokenFilter filter = new PatternReplaceFilter(tokenizer, Pattern.compile("(\\\\(\\\\| ))"), "$2", true);
+                       
+                       return new TokenStreamComponents(tokenizer, lowercase ? new LowercaseFilter(filter) : filter);
+               }
+               
+       }
+
+    private static AtomicReference<Pair<Query, String>> queryCache = new AtomicReference<>();
+
+    final static PerFieldAnalyzerWrapper analyzer = createAnalyzer();
+    
+    static PerFieldAnalyzerWrapper createAnalyzer() {
+       
+       Map<String,Analyzer> analyzerPerField = new HashMap<>();
+       analyzerPerField.put("Model", new KeywordAnalyzer());
+       analyzerPerField.put("Parent", new KeywordAnalyzer());
+       analyzerPerField.put("Resource", new KeywordAnalyzer());
+       analyzerPerField.put("GUID", new KeywordAnalyzer());
+       analyzerPerField.put("Name", new KeywordAnalyzer());
+       analyzerPerField.put("Types", new TypeStringAnalyzer(false));
+       analyzerPerField.put(Dependencies.FIELD_NAME_SEARCH, new LowerCaseWhitespaceAnalyzer(Version.LUCENE_4_9));
+       analyzerPerField.put(Dependencies.FIELD_TYPES_SEARCH, new TypeStringAnalyzer(true));
+       
+        PerFieldAnalyzerWrapper analyzer = new PerFieldAnalyzerWrapper(new LowerCaseWhitespaceAnalyzer(Version.LUCENE_4_9), analyzerPerField);
+        return analyzer;
+        
+    }
+
+    static PerFieldAnalyzerWrapper getAnalyzer() {
+       return analyzer;
+    }
+
+    static Query parse(String search, IndexSchema schema) throws ParseException {
+        Pair<Query, String> cachedQuery = queryCache.get();
+        if (cachedQuery != null && search.equals(cachedQuery.second))
+            return cachedQuery.first;
+
+        //System.err.println("parse " + search + " (cached=" + (cachedQuery != null ? cachedQuery.second : "null") + ")" );
+        CustomQueryParser parser = new CustomQueryParser(Version.LUCENE_4_9, "Name", getAnalyzer(), schema);
+        parser.setLowercaseExpandedTerms(false);
+        Query query = parser.parse(search);
+
+        queryCache.set(Pair.make(query, search));
+        return query;
+    }
+
+
+    public static class CustomQueryParser extends QueryParser {
+
+        protected final IndexSchema schema;
+
+        public CustomQueryParser(Version version, String field, Analyzer analyzer, IndexSchema schema) {
+            super(version, field, analyzer);
+            this.schema = schema;
+            setAllowLeadingWildcard(true);
+        }
+
+        @Override
+        protected Query getRangeQuery(
+                String field,
+                String part1,
+                String part2,
+                boolean startInclusive,
+                boolean endInclusive) throws ParseException
+        {
+            IndexSchema.Type type = schema.typeMap.get(field);
+            if (IndexSchema.NUMERIC_TYPES.contains(type)) {
+                boolean equalParts = ObjectUtils.objectEquals(part1, part2);
+                try {
+                    switch (type) {
+                    case INT: {
+                        Integer min = part1 != null ? (                   Integer.valueOf(part1)) : null;
+                        Integer max = part2 != null ? (equalParts ? min : Integer.valueOf(part2)) : null;
+                        return NumericRangeQuery.newIntRange(field, min, max, startInclusive, endInclusive);
+                    }
+                    case LONG: {
+                        Long min = part1 != null ? (                   Long.valueOf(part1)) : null;
+                        Long max = part2 != null ? (equalParts ? min : Long.valueOf(part2)) : null;
+                        return NumericRangeQuery.newLongRange(field, min, max, startInclusive, endInclusive);
+                    }
+                    case FLOAT: {
+                        Float min = part1 != null ? (                   Float.valueOf(part1)) : null;
+                        Float max = part2 != null ? (equalParts ? min : Float.valueOf(part2)) : null;
+                        return NumericRangeQuery.newFloatRange(field, min, max, startInclusive, endInclusive);
+                    }
+                    case DOUBLE: {
+                        Double min = part1 != null ? (                   Double.valueOf(part1)) : null;
+                        Double max = part2 != null ? (equalParts ? min : Double.valueOf(part2)) : null;
+                        return NumericRangeQuery.newDoubleRange(field, min, max, startInclusive, endInclusive);
+                    }
+                    default:
+                        throw new ParseException("Unrecognized numeric field type '" + type + "' for field '" + field + "'"); 
+                    }
+                } catch (NumberFormatException e) {
+                    throw new ParseException(e.getMessage()); 
+                }
+            }
+            return super.getRangeQuery(field, part1, part2, startInclusive, endInclusive); 
+        } 
+
+    }
+
+}