*/
package org.sonar.server.component.index;
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.common.unit.Fuzziness;
+import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.sonar.server.es.DefaultIndexSettings;
+import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.elasticsearch.index.query.QueryBuilders.prefixQuery;
import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_KEY;
PARTIAL {
@Override
public QueryBuilder getQuery(String queryText) {
+ BoolQueryBuilder query = boolQuery();
- // We will truncate the search to the maximum length of nGrams in the index.
- // Otherwise the search would for sure not find any results.
- String truncatedQuery = StringUtils.left(queryText, DefaultIndexSettings.MAXIMUM_NGRAM_LENGTH);
+ split(queryText)
+ .map(queryTerm -> {
- return matchQuery(SEARCH_GRAMS_ANALYZER.subField(FIELD_NAME), truncatedQuery);
+ // We will truncate the search to the maximum length of nGrams in the index.
+ // Otherwise the search would for sure not find any results.
+ String truncatedQuery = StringUtils.left(queryTerm, DefaultIndexSettings.MAXIMUM_NGRAM_LENGTH);
+ return matchQuery(SEARCH_GRAMS_ANALYZER.subField(FIELD_NAME), truncatedQuery);
+ })
+ .forEach(query::must);
+ return query;
}
},
FUZZY {
@Override
public QueryBuilder getQuery(String queryText) {
- return matchQuery(FUZZY_ANALYZER.subField(FIELD_NAME), queryText).fuzziness(Fuzziness.AUTO);
+ BoolQueryBuilder query = boolQuery();
+
+ split(queryText)
+ .map(((Function<String, QueryBuilder>) queryTerm -> matchQuery(FUZZY_ANALYZER.subField(FIELD_NAME), queryTerm).fuzziness(Fuzziness.AUTO))::apply)
+ .forEach(query::must);
+ return query;
}
},
FUZZY_PREFIX {
}
};
+ /** Pattern, that splits the user search input **/
+ public static final String SEARCH_TERM_TOKENIZER_PATTERN = "[\\:\\.\\s]+";
+
public abstract QueryBuilder getQuery(String queryText);
+
+ protected Stream<String> split(String queryText) {
+ return Arrays.stream(
+ queryText.split(SEARCH_TERM_TOKENIZER_PATTERN))
+ .filter(StringUtils::isNotEmpty);
+ }
}
import java.util.SortedMap;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.Settings.Builder;
+import org.sonar.server.component.index.ComponentIndexSearchFeature;
import static org.sonar.server.es.DefaultIndexSettings.ANALYSIS;
import static org.sonar.server.es.DefaultIndexSettings.ANALYZED;
import static org.sonar.server.es.DefaultIndexSettings.ANALYZER;
import static org.sonar.server.es.DefaultIndexSettings.ASCIIFOLDING;
-import static org.sonar.server.es.DefaultIndexSettings.CLASSIC;
import static org.sonar.server.es.DefaultIndexSettings.DELIMITER;
import static org.sonar.server.es.DefaultIndexSettings.FILTER;
import static org.sonar.server.es.DefaultIndexSettings.INDEX;
import static org.sonar.server.es.DefaultIndexSettings.KEYWORD;
-import static org.sonar.server.es.DefaultIndexSettings.LENGTH;
import static org.sonar.server.es.DefaultIndexSettings.LOWERCASE;
import static org.sonar.server.es.DefaultIndexSettings.MAXIMUM_NGRAM_LENGTH;
import static org.sonar.server.es.DefaultIndexSettings.MAX_GRAM;
import static org.sonar.server.es.DefaultIndexSettings.SUB_FIELD_DELIMITER;
import static org.sonar.server.es.DefaultIndexSettings.TOKENIZER;
import static org.sonar.server.es.DefaultIndexSettings.TRIM;
-import static org.sonar.server.es.DefaultIndexSettings.TRUNCATE;
import static org.sonar.server.es.DefaultIndexSettings.TYPE;
import static org.sonar.server.es.DefaultIndexSettings.WHITESPACE;
set(PATTERN, "\\.");
}
},
+ FUZZY_TOKENIZER(TOKENIZER) {
+
+ @Override
+ protected void setup() {
+ set(TYPE, PATTERN);
+ set(PATTERN, ComponentIndexSearchFeature.SEARCH_TERM_TOKENIZER_PATTERN);
+ }
+ },
// Analyzers
@Override
protected void setup() {
- set(TOKENIZER, CLASSIC);
+ set(TOKENIZER, FUZZY_TOKENIZER);
setArray(FILTER, LOWERCASE);
}
"sonaqube.java",
"sonqube.java");
}
+
+ @Test
+ public void fuzziness_for_filename_without_suffix() {
+ assertFileMatches("StringUtils",
+ "StringUtils.java",
+ "StringUils.java",
+ "StringUls.java");
+ }
}
assertFileMatches("factory abstract", "AbstractPluginFactory.java");
}
- @Test
- public void should_find_item_with_at_least_one_matching_word() {
- assertFileMatches("abstract object", "AbstractPluginFactory.java");
- }
-
@Test
public void should_require_at_least_one_matching_word() {
assertNoFileMatches("monitor object", "AbstractPluginFactory.java");
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.component.index;
+
+import org.junit.Test;
+
+public class ComponentIndexMultipleWordsTest extends ComponentIndexTest {
+
+ @Test
+ public void should_find_perfect_match() {
+ assertResultOrder("struts java",
+ "Struts.java");
+ }
+
+ @Test
+ public void should_find_fuzzy_match() {
+ features.set(ComponentIndexSearchFeature.FUZZY);
+ assertResultOrder("StrutX ProjecX",
+ "Struts Project");
+ }
+
+ @Test
+ public void should_find_partial_match() {
+ features.set(ComponentIndexSearchFeature.PARTIAL);
+ assertResultOrder("struts java",
+ "Xstrutsx.Xjavax");
+ }
+
+ @Test
+ public void should_find_partial_match_prefix_word1() {
+ assertResultOrder("struts java",
+ "MyStruts.java");
+ }
+
+ @Test
+ public void should_find_partial_match_suffix_word1() {
+ assertResultOrder("struts java",
+ "StrutsObject.java");
+ }
+
+ @Test
+ public void should_find_partial_match_prefix_word2() {
+ assertResultOrder("struts java",
+ "MyStruts.xjava");
+ }
+
+ @Test
+ public void should_find_partial_match_suffix_word2() {
+ assertResultOrder("struts java",
+ "MyStruts.javax");
+ }
+
+ @Test
+ public void should_find_partial_match_prefix_and_suffix_everywhere() {
+ assertResultOrder("struts java",
+ "MyStrutsObject.xjavax");
+ }
+
+ @Test
+ public void should_find_subset_of_document_terms() {
+ assertResultOrder("struts java",
+ "Some.Struts.Class.java.old");
+ }
+
+ @Test
+ public void should_require_all_words_to_match() {
+ assertNoFileMatches("struts java",
+ "Struts");
+ }
+
+ @Test
+ public void should_ignore_empty_words() {
+ assertFileMatches(" : . struts \n . :\n\n",
+ "Struts");
+ }
+
+ @Test
+ public void should_require_all_words_to_match_for_fuzziness() {
+ features.set(ComponentIndexSearchFeature.FUZZY);
+ assertNoFileMatches("struts java",
+ "Struts");
+ }
+
+ @Test
+ public void should_require_all_words_to_match_for_partial() {
+ features.set(ComponentIndexSearchFeature.PARTIAL);
+ assertNoFileMatches("struts java",
+ "Struts");
+ }
+
+}