[DS-1683] Add spell checker to discovery

This commit is contained in:
KevinVdV
2013-10-04 10:26:38 +02:00
parent 3db23c0987
commit 7dbd71543b
14 changed files with 185 additions and 31 deletions

View File

@@ -22,6 +22,7 @@ public class DiscoverQuery {
private List<String> filterQueries; private List<String> filterQueries;
private int DSpaceObjectFilter = -1; private int DSpaceObjectFilter = -1;
private List<String> fieldPresentQueries; private List<String> fieldPresentQueries;
private boolean spellCheck;
private int start = 0; private int start = 0;
private int maxResults = -1; private int maxResults = -1;
@@ -264,4 +265,12 @@ public class DiscoverQuery {
{ {
this.hitHighlighting.put(hitHighlighting.getField(), hitHighlighting); this.hitHighlighting.put(hitHighlighting.getField(), hitHighlighting);
} }
public boolean isSpellCheck() {
return spellCheck;
}
public void setSpellCheck(boolean spellCheck) {
this.spellCheck = spellCheck;
}
} }

View File

@@ -27,6 +27,7 @@ public class DiscoverResult {
private int maxResults = -1; private int maxResults = -1;
private int searchTime; private int searchTime;
private Map<String, DSpaceObjectHighlightResult> highlightedResults; private Map<String, DSpaceObjectHighlightResult> highlightedResults;
private String spellCheckQuery;
public DiscoverResult() { public DiscoverResult() {
@@ -150,6 +151,14 @@ public class DiscoverResult {
} }
} }
public String getSpellCheckQuery() {
return spellCheckQuery;
}
public void setSpellCheckQuery(String spellCheckQuery) {
this.spellCheckQuery = spellCheckQuery;
}
public static final class DSpaceObjectHighlightResult public static final class DSpaceObjectHighlightResult
{ {
private DSpaceObject dso; private DSpaceObject dso;

View File

@@ -59,10 +59,7 @@ import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.*;
import org.apache.solr.common.params.FacetParams;
import org.apache.solr.common.params.HighlightParams;
import org.apache.solr.common.params.MoreLikeThisParams;
import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.NamedList;
import org.dspace.content.Bitstream; import org.dspace.content.Bitstream;
import org.dspace.content.Bundle; import org.dspace.content.Bundle;
@@ -1574,6 +1571,12 @@ public class SolrServiceImpl implements SearchService, IndexingService {
} }
solrQuery.setQuery(query); solrQuery.setQuery(query);
if(discoveryQuery.isSpellCheck())
{
solrQuery.setParam(SpellingParams.SPELLCHECK_Q, query);
solrQuery.setParam(SpellingParams.SPELLCHECK_COLLATE, Boolean.TRUE);
solrQuery.setParam("spellcheck", Boolean.TRUE);
}
if (!includeWithdrawn) if (!includeWithdrawn)
{ {
@@ -1852,6 +1855,15 @@ public class SolrServiceImpl implements SearchService, IndexingService {
} }
} }
} }
if(solrQueryResponse.getSpellCheckResponse() != null)
{
String recommendedQuery = solrQueryResponse.getSpellCheckResponse().getCollatedResult();
if(StringUtils.isNotBlank(recommendedQuery))
{
result.setSpellCheckQuery(recommendedQuery);
}
}
} }
return result; return result;

View File

@@ -0,0 +1,29 @@
package org.dspace.discovery;
import org.apache.solr.common.SolrInputDocument;
import org.dspace.content.DCValue;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.core.Context;
/**
* Created with IntelliJ IDEA.
* User: kevin
* Date: 03/10/13
* Time: 15:06
* To change this template use File | Settings | File Templates.
*/
public class SolrServiceSpellIndexingPlugin implements SolrServiceIndexPlugin {
@Override
public void additionalIndex(Context context, DSpaceObject dso, SolrInputDocument document) {
if(dso instanceof Item){
DCValue[] dcValues = ((Item) dso).getMetadata(Item.ANY, Item.ANY, Item.ANY, Item.ANY);
for (DCValue dcValue : dcValues) {
document.addField("a_spell", dcValue.value);
}
}
}
}

View File

@@ -39,6 +39,7 @@ public class DiscoveryConfiguration implements InitializingBean{
private String id; private String id;
private DiscoveryHitHighlightingConfiguration hitHighlightingConfiguration; private DiscoveryHitHighlightingConfiguration hitHighlightingConfiguration;
private DiscoveryMoreLikeThisConfiguration moreLikeThisConfiguration; private DiscoveryMoreLikeThisConfiguration moreLikeThisConfiguration;
private boolean spellCheckEnabled;
public String getId() { public String getId() {
return id; return id;
@@ -122,6 +123,14 @@ public class DiscoveryConfiguration implements InitializingBean{
return moreLikeThisConfiguration; return moreLikeThisConfiguration;
} }
public boolean isSpellCheckEnabled() {
return spellCheckEnabled;
}
public void setSpellCheckEnabled(boolean spellCheckEnabled) {
this.spellCheckEnabled = spellCheckEnabled;
}
/** /**
* After all the properties are set check that the sidebar facets are a subset of our search filters * After all the properties are set check that the sidebar facets are a subset of our search filters
* *

View File

@@ -330,24 +330,7 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
Map<String, String> parameters = new HashMap<String, String>(); Map<String, String> parameters = new HashMap<String, String>();
parameters.put("page", "{pageNum}"); parameters.put("page", "{pageNum}");
String pageURLMask = generateURL(parameters); String pageURLMask = generateURL(parameters);
Map<String, String[]> filterQueryParams = getParameterFilterQueries(); pageURLMask = addFilterQueriesToUrl(pageURLMask);
if(filterQueryParams != null)
{
StringBuilder maskBuilder = new StringBuilder(pageURLMask);
for (String filterQueryParam : filterQueryParams.keySet())
{
String[] filterQueryValues = filterQueryParams.get(filterQueryParam);
if(filterQueryValues != null)
{
for (String filterQueryValue : filterQueryValues)
{
maskBuilder.append("&").append(filterQueryParam).append("=").append(filterQueryValue);
}
}
}
pageURLMask = maskBuilder.toString();
}
results.setMaskedPagination(itemsTotal, firstItemIndex, results.setMaskedPagination(itemsTotal, firstItemIndex,
lastItemIndex, currentPage, pagesTotal, pageURLMask); lastItemIndex, currentPage, pagesTotal, pageURLMask);
@@ -418,6 +401,28 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
//}// Empty query //}// Empty query
} }
protected String addFilterQueriesToUrl(String pageURLMask) {
Map<String, String[]> filterQueryParams = getParameterFilterQueries();
if(filterQueryParams != null)
{
StringBuilder maskBuilder = new StringBuilder(pageURLMask);
for (String filterQueryParam : filterQueryParams.keySet())
{
String[] filterQueryValues = filterQueryParams.get(filterQueryParam);
if(filterQueryValues != null)
{
for (String filterQueryValue : filterQueryValues)
{
maskBuilder.append("&").append(filterQueryParam).append("=").append(filterQueryValue);
}
}
}
pageURLMask = maskBuilder.toString();
}
return pageURLMask;
}
/** /**
* Render the given item, all metadata is added to the given list, which metadata will be rendered where depends on the xsl * Render the given item, all metadata is added to the given list, which metadata will be rendered where depends on the xsl
* @param dspaceObjectsList a list of DSpace objects * @param dspaceObjectsList a list of DSpace objects
@@ -818,6 +823,8 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
} }
} }
queryArgs.setSpellCheck(discoveryConfiguration.isSpellCheckEnabled());
this.queryResults = SearchUtils.getSearchService().search(context, scope, queryArgs); this.queryResults = SearchUtils.getSearchService().search(context, scope, queryArgs);
} }

View File

@@ -77,6 +77,7 @@ public class SimpleSearch extends AbstractSearch implements CacheableProcessingC
private static final Message T_filter_notequals = message("xmlui.Discovery.SimpleSearch.filter.notequals"); private static final Message T_filter_notequals = message("xmlui.Discovery.SimpleSearch.filter.notequals");
private static final Message T_filter_authority = message("xmlui.Discovery.SimpleSearch.filter.authority"); private static final Message T_filter_authority = message("xmlui.Discovery.SimpleSearch.filter.authority");
private static final Message T_filter_notauthority = message("xmlui.Discovery.SimpleSearch.filter.notauthority"); private static final Message T_filter_notauthority = message("xmlui.Discovery.SimpleSearch.filter.notauthority");
private static final Message T_did_you_mean = message("xmlui.Discovery.SimpleSearch.did_you_mean");
private SearchService searchService = null; private SearchService searchService = null;
@@ -145,6 +146,12 @@ public class SimpleSearch extends AbstractSearch implements CacheableProcessingC
Text text = searchBoxItem.addText("query"); Text text = searchBoxItem.addText("query");
text.setValue(queryString); text.setValue(queryString);
searchBoxItem.addButton("submit", "search-icon").setValue(T_go); searchBoxItem.addButton("submit", "search-icon").setValue(T_go);
if(queryResults != null && StringUtils.isNotBlank(queryResults.getSpellCheckQuery()))
{
Item didYouMeanItem = searchList.addItem("did-you-mean", "didYouMean");
didYouMeanItem.addContent(T_did_you_mean);
didYouMeanItem.addXref(getSuggestUrl(queryResults.getSpellCheckQuery()), queryResults.getSpellCheckQuery(), "didYouMean");
}
DSpaceObject dso = HandleUtil.obtainHandle(objectModel); DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(dso); DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(dso);
@@ -295,7 +302,7 @@ public class SimpleSearch extends AbstractSearch implements CacheableProcessingC
protected String generateURL(Map<String, String> parameters) protected String generateURL(Map<String, String> parameters)
throws UIException { throws UIException {
String query = getQuery(); String query = getQuery();
if (!"".equals(query)) if (!"".equals(query) && parameters.get("query") == null)
{ {
parameters.put("query", encodeForURL(query)); parameters.put("query", encodeForURL(query));
} }
@@ -382,4 +389,10 @@ public class SimpleSearch extends AbstractSearch implements CacheableProcessingC
} }
} }
} }
protected String getSuggestUrl(String newQuery) throws UIException {
Map parameters = new HashMap();
parameters.put("query", newQuery);
return addFilterQueriesToUrl(generateURL(parameters));
}
} }

View File

@@ -148,4 +148,6 @@
<message key="xmlui.Discovery.AbstractSearch.head2">Communities or Collections matching your query</message> <message key="xmlui.Discovery.AbstractSearch.head2">Communities or Collections matching your query</message>
<message key="xmlui.Discovery.AbstractSearch.head3">Items matching your query</message> <message key="xmlui.Discovery.AbstractSearch.head3">Items matching your query</message>
<message key="xmlui.Discovery.SimpleSearch.did_you_mean">Did you mean: </message>
</catalogue> </catalogue>

View File

@@ -1003,3 +1003,11 @@ div#aspect_discovery_SimpleSearch_div_search a.previous-page-link {
} }
/* End discovery layout DSpace 3.x*/ /* End discovery layout DSpace 3.x*/
.didYouMean{
font-size: 18px;
}
.didYouMean a{
font-weight: bold;
}

View File

@@ -1373,3 +1373,11 @@ table.discovery-filters th.new-filter-header
.searchTime{ .searchTime{
color: #999999; color: #999999;
} }
.didYouMean{
font-size: 18px;
}
.didYouMean a{
font-weight: bold;
}

View File

@@ -1450,3 +1450,11 @@ div.vocabulary-container li.error{
color: #c22121; color: #c22121;
} }
/* Controlled vocabulary support css END*/ /* Controlled vocabulary support css END*/
.didYouMean{
font-size: 18px;
}
.didYouMean a{
font-weight: bold;
}

View File

@@ -23,6 +23,7 @@
<context:annotation-config /> <!-- allows us to use spring annotations in beans --> <context:annotation-config /> <!-- allows us to use spring annotations in beans -->
<bean id="solrServiceResourceIndexPlugin" class="org.dspace.discovery.SolrServiceResourceRestrictionPlugin" scope="prototype"/> <bean id="solrServiceResourceIndexPlugin" class="org.dspace.discovery.SolrServiceResourceRestrictionPlugin" scope="prototype"/>
<bean id="SolrServiceSpellIndexingPlugin" class="org.dspace.discovery.SolrServiceSpellIndexingPlugin" scope="prototype"/>
<alias name="solrServiceResourceIndexPlugin" alias="org.dspace.discovery.SolrServiceResourceRestrictionPlugin"/> <alias name="solrServiceResourceIndexPlugin" alias="org.dspace.discovery.SolrServiceResourceRestrictionPlugin"/>
@@ -185,6 +186,8 @@
<property name="minWordLength" value="5"/> <property name="minWordLength" value="5"/>
</bean> </bean>
</property> </property>
<!-- When true a "did you mean" example will be displayed, value can be true or false -->
<property name="spellCheckEnabled" value="true"/>
</bean> </bean>

View File

@@ -455,6 +455,31 @@
<filter class="solr.TrimFilterFactory" /> <filter class="solr.TrimFilterFactory" />
</analyzer> </analyzer>
</fieldType> </fieldType>
<!--
SpellCheck analysis config based off of http://wiki.apache.org/solr/
SpellCheckingAnalysis
-->
<fieldType name="textSpell" class="solr.TextField"
positionIncrementGap="100" stored="false" multiValued="true">
<analyzer type="index">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.SynonymFilterFactory"
synonyms="synonyms.txt" ignoreCase="true"
expand="true"/>
<filter class="solr.StopFilterFactory" ignoreCase="true"
words="stopwords.txt"/>
<filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true"
words="stopwords.txt"/>
<filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
</analyzer>
</fieldType>
</types> </types>
@@ -517,6 +542,9 @@
<field name="location.comm" type="lowerCaseSort" indexed="true" stored="true" multiValued="true" required="false" omitNorms="true" /> <field name="location.comm" type="lowerCaseSort" indexed="true" stored="true" multiValued="true" required="false" omitNorms="true" />
<field name="location.coll" type="lowerCaseSort" indexed="true" stored="true" multiValued="true" required="false" omitNorms="true" /> <field name="location.coll" type="lowerCaseSort" indexed="true" stored="true" multiValued="true" required="false" omitNorms="true" />
<field name="a_spell" type="textSpell" />
<copyField source="fulltext" dest="a_spell" />
<!-- used by the DSpace Discovery Solr Indexer to track the last time a document was indexed --> <!-- used by the DSpace Discovery Solr Indexer to track the last time a document was indexed -->
<field name="SolrIndexer.lastIndexed" type="date" indexed="true" stored="true" default="NOW" multiValued="false" omitNorms="true" /> <field name="SolrIndexer.lastIndexed" type="date" indexed="true" stored="true" default="NOW" multiValued="false" omitNorms="true" />

View File

@@ -811,6 +811,9 @@
<int name="rows">10</int> <int name="rows">10</int>
<str name="df">search_text</str> <str name="df">search_text</str>
</lst> </lst>
<arr name="last-components">
<str>spellcheck</str>
</arr>
<!-- In addition to defaults, "appends" params can be specified <!-- In addition to defaults, "appends" params can be specified
to identify values which should be appended to the list of to identify values which should be appended to the list of
multi-val params from the query (or the existing "defaults"). multi-val params from the query (or the existing "defaults").
@@ -862,7 +865,9 @@
<str>nameOfCustomComponent1</str> <str>nameOfCustomComponent1</str>
<str>nameOfCustomComponent2</str> <str>nameOfCustomComponent2</str>
</arr> </arr>
--> -->
</requestHandler> </requestHandler>
<!-- A request handler that returns indented JSON by default --> <!-- A request handler that returns indented JSON by default -->
@@ -1232,10 +1237,14 @@
<str name="queryAnalyzerFieldType">textSpell</str> <str name="queryAnalyzerFieldType">textSpell</str>
<lst name="spellchecker"> <lst name="spellchecker">
<str name="classname">solr.IndexBasedSpellChecker</str>
<str name="name">default</str> <str name="name">default</str>
<str name="field">name</str> <str name="field">a_spell</str>
<str name="spellcheckIndexDir">./spellchecker</str> <str name="spellcheckIndexDir">./spellchecker</str>
<str name="buildOnCommit">true</str>
<str name="spellcheck.onlyMorePopular">false</str>
</lst> </lst>
<!-- a spellchecker that uses a different distance measure <!-- a spellchecker that uses a different distance measure