Merge pull request #141 from Alfresco/fix/SEARCH-1445_default_text_search_fields

Fix/search 1445 default text search fields
This commit is contained in:
eliaporciani
2019-04-08 21:26:44 +02:00
committed by GitHub
6 changed files with 405 additions and 168 deletions
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2005-2019 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco 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.
*
* Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.solr.query;
import org.alfresco.model.ContentModel;
import org.alfresco.service.namespace.QName;
/**
* @author elia
*
* Default fields for a text search.
*/
public enum AlfrescoDefaultTextFields
{
NAME(ContentModel.PROP_NAME),
TITLE(ContentModel.PROP_TITLE),
DESCRIPTION(ContentModel.PROP_DESCRIPTION),
CONTENT(ContentModel.PROP_CONTENT);
private QName field;
public String getFieldName()
{
return field.toString();
}
AlfrescoDefaultTextFields(QName field) {
this.field = field;
}
}
@@ -38,7 +38,8 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.dictionary.IndexTokenisationMode;
import org.alfresco.repo.search.MLAnalysisMode;
@@ -72,6 +73,7 @@ import org.alfresco.solr.AlfrescoSolrDataModel.IndexedField;
import org.alfresco.solr.SolrInformationServer;
import org.alfresco.solr.component.FingerPrintComponent;
import org.alfresco.solr.content.SolrContentStore;
import org.alfresco.solr.utils.ThrowingFunction;
import org.alfresco.util.CachingDateFormat;
import org.alfresco.util.Pair;
import org.alfresco.util.SearchLanguageConversion;
@@ -405,22 +407,7 @@ public class Solr4QueryParser extends QueryParser implements QueryConstants
throw new UnsupportedOperationException("Span is not supported for " + FIELD_PATHWITHREPEATS);
} else if (field.equals(FIELD_TEXT))
{
Set<String> text = searchParameters.getTextAttributes();
if ((text == null) || (text.size() == 0))
{
Query query = getSpanQuery(PROPERTY_FIELD_PREFIX + ContentModel.PROP_CONTENT.toString(), first, last,
slop, inOrder);
return query;
} else
{
BooleanQuery.Builder query = new BooleanQuery.Builder();
for (String fieldName : text)
{
Query part = getSpanQuery(fieldName, first, last, slop, inOrder);
query.add(part, Occur.SHOULD);
}
return query.build();
}
return createDefaultTextQuery(textField -> getSpanQuery(textField, first, last, slop, inOrder));
} else if (field.equals(FIELD_CLASS))
{
throw new UnsupportedOperationException("Span is not supported for " + FIELD_CLASS);
@@ -1485,35 +1472,46 @@ public class Solr4QueryParser extends QueryParser implements QueryConstants
return createTermQuery(field, queryText);
}
/**
*
* Get generic text query
*
*
*/
protected Query createDefaultTextQuery(ThrowingFunction<String, Query, ParseException> getQuery) throws ParseException
{
Set<String> text = searchParameters.getTextAttributes();
if (text == null || text.isEmpty())
{
text = Stream.of(AlfrescoDefaultTextFields.values())
.map(t -> PROPERTY_FIELD_PREFIX + t.getFieldName())
.collect(Collectors.toSet());
}
BooleanQuery.Builder query = new BooleanQuery.Builder();
for (String fieldName : text)
{
Query part = getQuery.apply(fieldName);
if (part != null)
{
query.add(part, Occur.SHOULD);
} else
{
query.add(createNoMatchQuery(), Occur.SHOULD);
}
}
return query.build();
}
protected Query createTextQuery(String queryText, AnalysisMode analysisMode, LuceneFunction luceneFunction)
throws ParseException
{
Set<String> text = searchParameters.getTextAttributes();
if ((text == null) || (text.size() == 0))
{
Query query = getFieldQuery(PROPERTY_FIELD_PREFIX + ContentModel.PROP_CONTENT.toString(), queryText,
analysisMode, luceneFunction);
if (query == null)
{
return createNoMatchQuery();
}
return query;
} else
{
BooleanQuery.Builder query = new BooleanQuery.Builder();
for (String fieldName : text)
{
Query part = getFieldQuery(fieldName, queryText, analysisMode, luceneFunction);
if (part != null)
{
query.add(part, Occur.SHOULD);
} else
{
query.add(createNoMatchQuery(), Occur.SHOULD);
}
}
return query.build();
}
return createDefaultTextQuery(textField -> getFieldQuery(textField, queryText, analysisMode, luceneFunction));
}
@SuppressWarnings("unchecked")
@@ -2951,34 +2949,10 @@ public class Solr4QueryParser extends QueryParser implements QueryConstants
throw new UnsupportedOperationException("Range Queries are not support for " + FIELD_PATHWITHREPEATS);
} else if (field.equals(FIELD_TEXT))
{
Set<String> text = searchParameters.getTextAttributes();
if ((text == null) || (text.size() == 0))
{
Query query = getRangeQuery(PROPERTY_FIELD_PREFIX + ContentModel.PROP_CONTENT.toString(), part1, part2,
includeLower, includeUpper, analysisMode, luceneFunction);
if (query == null)
{
return createNoMatchQuery();
}
return query;
} else
{
BooleanQuery.Builder query = new BooleanQuery.Builder();
for (String fieldName : text)
{
Query part = getRangeQuery(fieldName, part1, part2, includeLower, includeUpper, analysisMode,
luceneFunction);
if (part != null)
{
query.add(part, Occur.SHOULD);
} else
{
query.add(createNoMatchQuery(), Occur.SHOULD);
}
}
return query.build();
}
return createDefaultTextQuery(textField -> getRangeQuery(textField, part1, part2, includeLower,
includeUpper, analysisMode, luceneFunction));
} else if (field.equals(FIELD_CASCADETX))
{
@@ -3384,33 +3358,9 @@ public class Solr4QueryParser extends QueryParser implements QueryConstants
throw new UnsupportedOperationException("Prefix Queries are not support for " + FIELD_PATHWITHREPEATS);
} else if (field.equals(FIELD_TEXT))
{
Set<String> text = searchParameters.getTextAttributes();
if ((text == null) || (text.size() == 0))
{
Query query = getPrefixQuery(PROPERTY_FIELD_PREFIX + ContentModel.PROP_CONTENT.toString(), termStr,
analysisMode);
if (query == null)
{
return createNoMatchQuery();
}
return query;
} else
{
BooleanQuery.Builder query = new BooleanQuery.Builder();
for (String fieldName : text)
{
Query part = getPrefixQuery(fieldName, termStr, analysisMode);
if (part != null)
{
query.add(part, Occur.SHOULD);
} else
{
query.add(createNoMatchQuery(), Occur.SHOULD);
}
}
return query.build();
}
return createDefaultTextQuery(textField -> getPrefixQuery(textField, termStr, analysisMode));
} else if (field.equals(FIELD_ID))
{
boolean lowercaseExpandedTerms = getLowercaseExpandedTerms();
@@ -3572,33 +3522,9 @@ public class Solr4QueryParser extends QueryParser implements QueryConstants
throw new UnsupportedOperationException("Wildcard Queries are not support for " + FIELD_PATHWITHREPEATS);
} else if (field.equals(FIELD_TEXT))
{
Set<String> text = searchParameters.getTextAttributes();
if ((text == null) || (text.size() == 0))
{
Query query = getWildcardQuery(PROPERTY_FIELD_PREFIX + ContentModel.PROP_CONTENT.toString(), termStr,
analysisMode);
if (query == null)
{
return createNoMatchQuery();
}
return query;
} else
{
BooleanQuery.Builder query = new BooleanQuery.Builder();
for (String fieldName : text)
{
Query part = getWildcardQuery(fieldName, termStr, analysisMode);
if (part != null)
{
query.add(part, Occur.SHOULD);
} else
{
query.add(createNoMatchQuery(), Occur.SHOULD);
}
}
return query.build();
}
return createDefaultTextQuery(textField -> getWildcardQuery(textField, termStr, analysisMode));
} else if (field.equals(FIELD_ID))
{
boolean lowercaseExpandedTerms = getLowercaseExpandedTerms();
@@ -3755,33 +3681,9 @@ public class Solr4QueryParser extends QueryParser implements QueryConstants
throw new UnsupportedOperationException("Fuzzy Queries are not support for " + FIELD_PATHWITHREPEATS);
} else if (field.equals(FIELD_TEXT))
{
Set<String> text = searchParameters.getTextAttributes();
if ((text == null) || (text.size() == 0))
{
Query query = getFuzzyQuery(PROPERTY_FIELD_PREFIX + ContentModel.PROP_CONTENT.toString(), termStr,
minSimilarity);
if (query == null)
{
return createNoMatchQuery();
}
return query;
} else
{
BooleanQuery.Builder query = new BooleanQuery.Builder();
for (String fieldName : text)
{
Query part = getFuzzyQuery(fieldName, termStr, minSimilarity);
if (part != null)
{
query.add(part, Occur.SHOULD);
} else
{
query.add(createNoMatchQuery(), Occur.SHOULD);
}
}
return query.build();
}
return createDefaultTextQuery(textField -> getFuzzyQuery(textField, termStr, minSimilarity));
} else if (field.equals(FIELD_ID) || field.equals(FIELD_DBID) || field.equals(FIELD_ISROOT)
|| field.equals(FIELD_ISCONTAINER) || field.equals(FIELD_ISNODE) || field.equals(FIELD_TX)
|| field.equals(FIELD_PARENT) || field.equals(FIELD_PRIMARYPARENT) || field.equals(FIELD_QNAME)
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2005-2019 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco 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.
*
* Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.solr.utils;
/**
* @author elia
*
* Class used to use a lambda throwing a catched exceptino
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*
*/
@FunctionalInterface
public interface ThrowingFunction<T, R, E extends Throwable> {
R apply(T t) throws E;
}
@@ -310,23 +310,6 @@ public class AlfrescoHighlighterTest extends AbstractAlfrescoSolrTests
logger.info("######### MultiTerm ###########");
req = areq(params( "q", "name:long", "qt", "/afts", "start", "0", "rows", "5",
HighlightParams.HIGHLIGHT, "true",
HighlightParams.Q, "lon*",
HighlightParams.FIELDS, "name",
HighlightParams.HIGHLIGHT_MULTI_TERM, "false",
HighlightParams.SIMPLE_PRE, "{",
HighlightParams.SIMPLE_POST, "}",
HighlightParams.SNIPPETS, String.valueOf(1),
HighlightParams.FRAGSIZE, String.valueOf(100)),
"{\"locales\":[\"en\"], \"tenants\": [ \"\" ]}");
assertQ(req,
"*[count(//lst[@name='highlighting']/lst)=2]",
"*[count(//lst[@name='highlighting']/lst/arr[@name='title'])=0]",
"*[count(//lst[@name='highlighting']/lst/arr[@name='name'])=0]");
logger.info("######### CamelCase ###########");
@@ -293,7 +293,8 @@ public class QParserPluginTest extends AbstractQParserPluginTest implements Quer
assertAQuery("TEXT:\"alf?????\"", 1);
assertAQuery("TEXT:\"al??????\"", 1);
assertAQuery("TEXT:\"a???????\"", 1);
assertAQuery("TEXT:\"????????\"", 1);
// it finds thirteen and fourteen in cm:names. file fourteen contains alfresco in content.
assertAQuery("TEXT:\"????????\"", 2);
assertAQuery("TEXT:\"a??re???\"", 1);
assertAQuery("TEXT:\"?lfresco\"", 1);
assertAQuery("TEXT:\"??fresco\"", 1);
@@ -321,7 +322,8 @@ public class QParserPluginTest extends AbstractQParserPluginTest implements Quer
assertAQuery("TEXT:\"*esco\"", 1);
assertAQuery("TEXT:\"*sco\"", 1);
assertAQuery("TEXT:\"*co\"", 1);
assertAQuery("TEXT:\"*o\"", 1);
// it finds cm:name:two and file fourteen because of content
assertAQuery("TEXT:\"*o\"", 2);
assertAQuery("TEXT:\"****lf**sc***\"", 1);
assertAQuery("TEXT:\"??lf**sc***\"", 0);
assertAQuery("TEXT:\"alfresc*tutorial\"", 0);
@@ -426,8 +428,10 @@ public class QParserPluginTest extends AbstractQParserPluginTest implements Quer
@Test
public void nonFields()
{
/* the query is executed on cm:name, cm:title, cm:description and cm:content
* TEXT:fo* finds nodes with names four and fourteen */
assertAQuery("TEXT:fox", 1);
assertAQuery("TEXT:fo*", 1);
assertAQuery("TEXT:fo*", 2);
assertAQuery("TEXT:f*x", 1);
assertAQuery("TEXT:*ox", 1);
@@ -0,0 +1,264 @@
/*
* Copyright (C) 2005-2019 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco 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.
*
* Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.solr.query.afts.requestHandler;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName;
import org.alfresco.solr.client.PropertyValue;
import org.alfresco.solr.client.StringPropertyValue;
import org.alfresco.solr.query.afts.TestDataProvider;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.Arrays.asList;
import static java.util.stream.IntStream.range;
import static org.alfresco.model.ContentModel.*;
import static org.alfresco.solr.AlfrescoSolrUtils.addNode;
import static com.google.common.collect.ImmutableMap.of;
/**
* @author elia
*
* The following test set needs to test that default text queries actually work by searching
* in cm:name, cm:title, cm:description and cm:content fields.
*
* THe default queries can be of the following types:
* exact, prefix, wildcard, fuzzy, span and range.
* This test set checks that in all these query types the default fields are involved in the search.
*
*/
public class AFTSDefaultTextQueryTest extends AbstractRequestHandlerTest
{
@BeforeClass
public static void beforeClass() throws Exception
{
TestDataProvider dataProvider = new TestDataProvider(h);
List<Map<String, String>> data = asList(
of("name", "test1",
"description", "description of test 1",
"content", "test",
"title", "TITLE1",
"creator", "Luca"),
of("name", "test2",
"description", "description 2",
"content", "content of test 2",
"title", "Other Title",
"creator", "Mario"),
of("name", "file3",
"description", "this is not a description of test 1 and 2",
"content", "other content here ",
"title", "Third",
"creator", "Giovanni"),
of("name", "name of record 4",
"description", "other description right here",
"content", "content of file number 4",
"title", "Forth",
"creator", "Giuseppe"));
TEST_ROOT_NODEREF = dataProvider.getRootNode();
range(0, data.size())
.forEach(dbId -> {
Map<String, String> record = data.get(dbId);
String name = record.get("name");
String description = record.get("description");
String content = record.get("content");
String title = record.get("title");
String creator = record.get("creator");
Map<QName, PropertyValue> properties = new HashMap<>();
properties.put(PROP_NAME, new StringPropertyValue(name));
properties.put(PROP_DESCRIPTION, new StringPropertyValue(description));
properties.put(PROP_CONTENT, new StringPropertyValue(content));
properties.put(PROP_TITLE, new StringPropertyValue(title));
properties.put(PROP_CREATOR, new StringPropertyValue(creator));
addNode(getCore(),
dataModel, 1, dbId, 1,
TYPE_CONTENT, null, properties, null,
"the_owner_of_this_node_is" + name,
null,
new NodeRef[]{ TEST_ROOT_NODEREF },
new String[]{ "/" + dataProvider.qName("a_qname_for_node_" + name) },
dataProvider.newNodeRef(), true);
});
}
/**
* Test exact search is working.
*/
@Test
public void defaultExactQueryTest()
{
/*
* 3 results expected:
* record 1 ("test" in name, description and content)
* record 2 ("test" in name, and content)
* record 3 ("test" in description)
*/
assertResponseCardinality("test", 3);
/*
* 3 results expected:
* record 2 ("Other" in title )
* record 3 ("other" in content)
* record 4 ("other" in description)
*/
assertResponseCardinality("Other", 3);
/*
* No results expected because creator should not be considered in default text search.
*/
assertResponseCardinality("Giovanni", 0);
}
/**
* Test range queries.
*/
@Test
public void defaultRangeQueryTest()
{
/*
* 3 results expected:
* record 1 ("test" in name, description and content)
* record 2 ("test" in name, and content)
* record 3 ("test" in description)
*/
assertResponseCardinality("[te to test]", 3);
/*
* 3 results expected:
* record 2 ("Other" in title )
* record 3 ("other" in content)
* record 4 ("other" in description)
*/
assertResponseCardinality("other to otherz ", 3);
}
/**
* Test wildcard queries.
*/
@Test
public void defaultWildCardQueryTest()
{
/*
* 3 results expected:
* record 1 ("test" in name, description and content)
* record 2 ("test" in name, and content)
* record 3 ("test" in description)
*/
assertResponseCardinality("?est", 3);
/*
* 3 results expected:
* record 2 ("Other" in title )
* record 3 ("other" in content)
* record 4 ("other" in description)
*/
assertResponseCardinality("?ther", 3);
}
/**
* Thes span queries
*/
@Test
public void defaultSpanQueryTest()
{
/*
* 1 result expected
* record 1 ("Other title" in title)
*/
assertResponseCardinality("Other *(0) title", 1);
/*
* 2 results expected
* record 1 ("description of test" in description)
* record 3 ("description of test" in description)
*/
assertResponseCardinality("description *(1) test ", 2);
/*
* No results expected.
* There is no "description test" in any field.
*/
assertResponseCardinality("description *(0) test ", 0);
/*
* 1 result expected
* record 4 ("name of record" in name)
*/
assertResponseCardinality("name *(1) record ", 1);
/*
* 2 results expected
* record 2 ("other content here" in content)
*/
assertResponseCardinality("other *(1) here ", 1);
}
/**
* Test prefix queries
*/
@Test
public void defaultPrefixQueryTest()
{
/*
* 3 results expected
* record 1 ("test" in name, description and content)
* record 2 ("test" in name, and content)
* record 3 ("test" in description)
*/
assertResponseCardinality("te*", 3);
/*
* 3 results expected
* record 1 ("file" in name, description and content)
* record 2 ("file" in name, and content)
*/
assertResponseCardinality("fil*", 2);
/*
* 3 results expected:
* record 2 ("Other" in title )
* record 3 ("other" in content)
* record 4 ("other" in description)
*/
assertResponseCardinality("oth*", 3);
}
}