NIHVIVO-3467 Refactor to use SolrQueryUtils.

This commit is contained in:
j2blake 2012-01-17 17:27:47 +00:00
parent b09c4b309c
commit 0dde805f3c
6 changed files with 371 additions and 144 deletions

View file

@ -2,18 +2,22 @@
package edu.cornell.mannlib.vitro.webapp.controller.accounts.manageproxies.ajax; package edu.cornell.mannlib.vitro.webapp.controller.accounts.manageproxies.ajax;
import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.AC_NAME_STEMMED;
import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.NAME_LOWERCASE_SINGLE_VALUED;
import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.NAME_RAW;
import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.NAME_UNSTEMMED;
import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.RDFTYPE;
import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.URI;
import static edu.cornell.mannlib.vitro.webapp.utils.solr.SolrQueryUtils.Conjunction.OR;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrQuery;
@ -21,16 +25,15 @@ import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.client.solrj.SolrServer; import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.ajax.AbstractAjaxResponder; import edu.cornell.mannlib.vitro.webapp.controller.ajax.AbstractAjaxResponder;
import edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames;
import edu.cornell.mannlib.vitro.webapp.search.solr.SolrSetup; import edu.cornell.mannlib.vitro.webapp.search.solr.SolrSetup;
import edu.cornell.mannlib.vitro.webapp.utils.solr.AutoCompleteWords;
import edu.cornell.mannlib.vitro.webapp.utils.solr.FieldMap;
import edu.cornell.mannlib.vitro.webapp.utils.solr.SolrQueryUtils;
/** /**
* Get the basic auto-complete info for the profile selection. * Get the basic auto-complete info for the profile selection.
@ -39,63 +42,42 @@ import edu.cornell.mannlib.vitro.webapp.search.solr.SolrSetup;
public class BasicProfilesGetter extends AbstractAjaxResponder { public class BasicProfilesGetter extends AbstractAjaxResponder {
private static final Log log = LogFactory.getLog(BasicProfilesGetter.class); private static final Log log = LogFactory.getLog(BasicProfilesGetter.class);
private static final String WORD_DELIMITER = "[, ]+";
private static final FieldMap RESPONSE_FIELDS = SolrQueryUtils
.fieldMap().put(URI, "uri").put(NAME_RAW, "label")
.put("bogus", "classLabel").put("bogus", "imageUrl");
private static final String PROPERTY_PROFILE_TYPES = "proxy.eligibleTypeList"; private static final String PROPERTY_PROFILE_TYPES = "proxy.eligibleTypeList";
private static final String PARAMETER_SEARCH_TERM = "term"; private static final String PARAMETER_SEARCH_TERM = "term";
private static final String DEFAULT_PROFILE_TYPES = "http://www.w3.org/2002/07/owl#Thing"; private static final String DEFAULT_PROFILE_TYPES = "http://www.w3.org/2002/07/owl#Thing";
private final String term; private final String term;
private final List<String> completeWords; private final AutoCompleteWords searchWords;
private final String partialWord; private final List<String> profileTypes;
private final Collection<String> profileTypes;
public BasicProfilesGetter(HttpServlet servlet, VitroRequest vreq, public BasicProfilesGetter(HttpServlet servlet, VitroRequest vreq,
HttpServletResponse resp) { HttpServletResponse resp) {
super(servlet, vreq, resp); super(servlet, vreq, resp);
this.term = getStringParameter(PARAMETER_SEARCH_TERM, ""); this.term = getStringParameter(PARAMETER_SEARCH_TERM, "");
this.searchWords = SolrQueryUtils.parseForAutoComplete(term,
List<String> termWords = figureTermWords(); WORD_DELIMITER);
if (termWords.isEmpty() || this.term.endsWith(" ")) {
this.completeWords = termWords;
this.partialWord = null;
} else {
this.completeWords = termWords.subList(0, termWords.size() - 1);
this.partialWord = termWords.get(termWords.size() - 1);
}
this.profileTypes = figureProfileTypes(); this.profileTypes = figureProfileTypes();
log.debug(this); log.debug(this);
} }
private List<String> figureTermWords() { private List<String> figureProfileTypes() {
List<String> list = new ArrayList<String>();
String[] array = this.term.split("[, ]+");
for (String word : array) {
String trimmed = word.trim();
if (!trimmed.isEmpty()) {
list.add(trimmed);
}
}
return Collections.unmodifiableList(list);
}
private Collection<String> figureProfileTypes() {
List<String> list = new ArrayList<String>();
String typesString = ConfigurationProperties.getBean(vreq).getProperty( String typesString = ConfigurationProperties.getBean(vreq).getProperty(
PROPERTY_PROFILE_TYPES, DEFAULT_PROFILE_TYPES); PROPERTY_PROFILE_TYPES, DEFAULT_PROFILE_TYPES);
String[] types = typesString.split(","); List<String> list = SolrQueryUtils.parseWords(typesString,
for (String type : types) { WORD_DELIMITER);
String trimmed = type.trim();
if (!trimmed.isEmpty()) {
list.add(trimmed);
}
}
if (list.isEmpty()) { if (list.isEmpty()) {
log.error("No types configured for profile pages in " log.error("No types configured for profile pages in "
+ PROPERTY_PROFILE_TYPES); + PROPERTY_PROFILE_TYPES);
} }
return Collections.unmodifiableCollection(list); return list;
} }
@Override @Override
@ -106,13 +88,15 @@ public class BasicProfilesGetter extends AbstractAjaxResponder {
} }
try { try {
SolrServer solr = SolrSetup.getSolrServer(servlet ServletContext ctx = servlet.getServletContext();
.getServletContext()); SolrServer solr = SolrSetup.getSolrServer(ctx);
SolrQuery query = buildSolrQuery(); SolrQuery query = buildSolrQuery();
QueryResponse queryResponse = solr.query(query); QueryResponse queryResponse = solr.query(query);
JSONArray jsonArray = parseResponse(queryResponse); List<Map<String, String>> parsed = SolrQueryUtils
String response = jsonArray.toString(); .parseResponse(queryResponse, RESPONSE_FIELDS);
String response = assembleJsonResponse(parsed);
log.debug(response); log.debug(response);
return response; return response;
} catch (SolrServerException e) { } catch (SolrServerException e) {
@ -123,110 +107,20 @@ public class BasicProfilesGetter extends AbstractAjaxResponder {
private SolrQuery buildSolrQuery() { private SolrQuery buildSolrQuery() {
SolrQuery q = new SolrQuery(); SolrQuery q = new SolrQuery();
q.setFields(VitroSearchTermNames.NAME_RAW, VitroSearchTermNames.URI); q.setFields(NAME_RAW, URI);
q.setSortField(VitroSearchTermNames.NAME_LOWERCASE_SINGLE_VALUED, q.setSortField(NAME_LOWERCASE_SINGLE_VALUED, ORDER.asc);
ORDER.asc); q.setFilterQueries(SolrQueryUtils.assembleConjunctiveQuery(RDFTYPE,
q.setFilterQueries(assembleTypeRestrictionQuery()); profileTypes, OR));
q.setStart(0); q.setStart(0);
q.setRows(30); q.setRows(30);
q.setQuery(buildQueryStringFromSearchTerm()); q.setQuery(searchWords.assembleQuery(NAME_UNSTEMMED, AC_NAME_STEMMED));
return q; return q;
// use VitroSearchTermNames.NAME_LOWERCASE
// break the search term into words, then insert AND between the words
// TODO Auto-generated method stub
}
private String assembleTypeRestrictionQuery() {
List<String> terms = new ArrayList<String>();
for (String profileType : profileTypes) {
terms.add(VitroSearchTermNames.RDFTYPE + ":\"" + profileType + "\"");
}
String q = StringUtils.join(terms, " OR ");
log.debug("Type restriction query is '" + q + "'");
return q;
}
private String buildQueryStringFromSearchTerm() {
List<String> terms = new ArrayList<String>();
for (String word : completeWords) {
terms.add(termForCompleteWord(word));
}
if (partialWord != null) {
terms.add(termForPartialWord(partialWord));
}
String q = StringUtils.join(terms, " AND ");
log.debug("Query string is '" + q + "'");
return q;
}
private String termForCompleteWord(String word) {
return VitroSearchTermNames.NAME_UNSTEMMED + ":\"" + word + "\"";
}
private String termForPartialWord(String word) {
return VitroSearchTermNames.AC_NAME_STEMMED + ":\"" + word + "\"";
}
private JSONArray parseResponse(QueryResponse queryResponse) {
JSONArray jsonArray = new JSONArray();
if (queryResponse == null) {
log.error("Query response for a search was null");
return jsonArray;
}
SolrDocumentList docs = queryResponse.getResults();
if (docs == null) {
log.error("Docs for a search was null");
return jsonArray;
}
long hitCount = docs.getNumFound();
log.debug("Total number of hits = " + hitCount);
if (hitCount < 1) {
return jsonArray;
}
for (SolrDocument doc : docs) {
try {
String uri = doc.get(VitroSearchTermNames.URI).toString();
Object nameRaw = doc.get(VitroSearchTermNames.NAME_RAW);
String name = null;
if (nameRaw instanceof List<?>) {
@SuppressWarnings("unchecked")
List<String> nameRawList = (List<String>) nameRaw;
name = nameRawList.get(0);
} else {
name = (String) nameRaw;
}
jsonArray.put(resultRow(uri, name));
} catch (Exception e) {
log.error("problem getting usable individuals from search "
+ "hits" + e.getMessage());
}
}
return jsonArray;
}
private Map<String, String> resultRow(String uri, String name) {
Map<String, String> map = new HashMap<String, String>();
map.put("uri", uri);
map.put("label", name);
map.put("classLabel", "");
map.put("imageUrl", "");
return map;
} }
@Override @Override
public String toString() { public String toString() {
return "BasicProfilesGetter[term=" + term + ", completeWords=" return "BasicProfilesGetter[term=" + term + ", searchWords="
+ completeWords + ", partialWord=" + partialWord + searchWords + ", profileTypes=" + profileTypes + "]";
+ ", profileTypes=" + profileTypes + "]";
} }
} }

View file

@ -7,6 +7,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
@ -75,6 +76,18 @@ public abstract class AbstractAjaxResponder {
} }
} }
/**
* Assemble a list of maps into a single String representing a JSON array of
* objects with fields.
*/
protected String assembleJsonResponse(List<Map<String, String>> maps) {
JSONArray jsonArray = new JSONArray();
for (Map<String, String> map: maps) {
jsonArray.put(map);
}
return jsonArray.toString();
}
/** /**
* AJAX responders can use a parser that extends this class. The parser must * AJAX responders can use a parser that extends this class. The parser must
* implement "parseSolutionRow()" * implement "parseSolutionRow()"

View file

@ -0,0 +1,87 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.solr;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A helper class for use with an Auto-complete query.
*
* Any word that is followed by a delimiter is considered to be complete, and
* should be matched exactly in the query. If there is a word on the end that is
* not followed by a delimiter, it is incomplete, and should act like a
* "starts-with" query.
*/
public class AutoCompleteWords {
private static final Log log = LogFactory.getLog(AutoCompleteWords.class);
private final String searchTerm;
private final String delimiterPattern;
private final List<String> completeWords;
private final String partialWord;
/**
* Package-access. Use SolrQueryUtils.parseForAutoComplete() to create an
* instance.
*/
AutoCompleteWords(String searchTerm, String delimiterPattern) {
this.searchTerm = searchTerm;
this.delimiterPattern = delimiterPattern;
List<String> termWords = figureTermWords();
if (termWords.isEmpty() || this.searchTerm.endsWith(" ")) {
this.completeWords = termWords;
this.partialWord = null;
} else {
this.completeWords = termWords.subList(0, termWords.size() - 1);
this.partialWord = termWords.get(termWords.size() - 1);
}
}
private List<String> figureTermWords() {
List<String> list = new ArrayList<String>();
String[] array = this.searchTerm.split(this.delimiterPattern);
for (String word : array) {
String trimmed = word.trim();
if (!trimmed.isEmpty()) {
list.add(trimmed);
}
}
return Collections.unmodifiableList(list);
}
public String assembleQuery(String fieldNameForCompleteWords,
String fieldNameForPartialWord) {
List<String> terms = new ArrayList<String>();
for (String word : this.completeWords) {
terms.add(buildTerm(fieldNameForCompleteWords, word));
}
if (partialWord != null) {
terms.add(buildTerm(fieldNameForPartialWord, partialWord));
}
String q = StringUtils.join(terms, " AND ");
log.debug("Query string is '" + q + "'");
return q;
}
private String buildTerm(String fieldName, String word) {
return fieldName + ":\"" + word + "\"";
}
@Override
public String toString() {
return "AutoCompleteWords[searchTerm='" + searchTerm
+ "', delimiterPattern='" + delimiterPattern
+ "', completeWords=" + completeWords + ", partialWord="
+ partialWord + "]";
}
}

View file

@ -0,0 +1,41 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.solr;
import java.util.HashMap;
import java.util.Map;
/**
* A builder object that can assemble a map of Solr field names to JSON field
* names.
*
* Use like this:
*
* m = SolrQueryUtils.fieldMap().row("this", "that").row("2nd", "row").map();
*
*/
public class FieldMap {
private final Map<String, String> m = new HashMap<String, String>();
/**
* Add a row to the map
*/
public FieldMap put(String solrFieldName, String jsonFieldName) {
if (solrFieldName == null) {
throw new NullPointerException("solrFieldName may not be null.");
}
if (jsonFieldName == null) {
throw new NullPointerException("jsonFieldName may not be null.");
}
m.put(solrFieldName, jsonFieldName);
return this;
}
/**
* Release the map for use.
*/
public Map<String, String> map() {
return new HashMap<String, String>(m);
}
}

View file

@ -0,0 +1,89 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.solr;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.solr.client.solrj.response.QueryResponse;
/**
* Some static method to help in constructing Solr queries and parsing the
* results.
*/
public class SolrQueryUtils {
public enum Conjunction {
AND, OR;
public String joiner() {
return " " + this.name() + " ";
}
}
/**
* Create an AutoCompleteWords object that can be used to build an
* auto-complete query.
*/
public static AutoCompleteWords parseForAutoComplete(String searchTerm,
String delimiterPattern) {
return new AutoCompleteWords(searchTerm, delimiterPattern);
}
/**
* Create a builder object that can assemble a map of Solr field names to
* JSON field names.
*/
public static FieldMap fieldMap() {
return new FieldMap();
}
/**
* Parse a response into a list of maps, one map for each document.
*
* The Solr field names in the document are replaced by json field names in
* the result, according to the fieldMap.
*/
public static List<Map<String, String>> parseResponse(
QueryResponse queryResponse, FieldMap fieldMap) {
return new SolrResultsParser(queryResponse, fieldMap).parse();
}
/**
* Break a string into a list of words, according to a RegEx delimiter. Trim
* leading and trailing white space from each word.
*/
public static List<String> parseWords(String typesString,
String wordDelimiter) {
List<String> list = new ArrayList<String>();
String[] array = typesString.split(wordDelimiter);
for (String word : array) {
String trimmed = word.trim();
if (!trimmed.isEmpty()) {
list.add(trimmed);
}
}
return list;
}
/**
* Glue these words together into a query on a given field, joined by either
* AND or OR.
*/
public static String assembleConjunctiveQuery(String fieldName,
Collection<String> words, Conjunction c) {
List<String> terms = new ArrayList<String>();
for (String word : words) {
terms.add(buildTerm(fieldName, word));
}
String q = StringUtils.join(terms, c.joiner());
return q;
}
private static String buildTerm(String fieldName, String word) {
return fieldName + ":\"" + word + "\"";
}
}

View file

@ -0,0 +1,103 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.solr;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
/**
* Parse this Solr response, creating a map of values for each document.
*
* The Solr field names in the document are replaced by json field names in the
* parsed results, according to the fieldMap.
*/
public class SolrResultsParser {
private static final Log log = LogFactory.getLog(SolrResultsParser.class);
private final QueryResponse queryResponse;
private final Map<String, String> fieldNameMapping;
public SolrResultsParser(QueryResponse queryResponse, FieldMap fieldMap) {
this.queryResponse = queryResponse;
this.fieldNameMapping = fieldMap.map();
}
/**
* Parse the entire response into a list of maps.
*/
public List<Map<String, String>> parse() {
List<Map<String, String>> maps = new ArrayList<Map<String, String>>();
if (queryResponse == null) {
log.error("Query response for a search was null");
return maps;
}
SolrDocumentList docs = queryResponse.getResults();
if (docs == null) {
log.error("Docs for a search was null");
return maps;
}
log.debug("Total number of hits = " + docs.getNumFound());
for (SolrDocument doc : docs) {
maps.add(parseSingleDocument(doc));
}
return maps;
}
/**
* Create a map from this document, applying translation on the field names.
*/
private Map<String, String> parseSingleDocument(SolrDocument doc) {
Map<String, String> result = new HashMap<String, String>();
for (String solrFieldName : fieldNameMapping.keySet()) {
String jsonFieldName = fieldNameMapping.get(solrFieldName);
result.put(jsonFieldName, parseSingleValue(doc, solrFieldName));
}
return result;
}
/**
* Find a single value in the document
*/
private String parseSingleValue(SolrDocument doc, String key) {
Object rawValue = getFirstValue(doc.get(key));
if (rawValue == null) {
return "";
}
if (rawValue instanceof String) {
return (String) rawValue;
}
return String.valueOf(rawValue);
}
/**
* The result might be a list. If so, get the first element.
*/
private Object getFirstValue(Object rawValue) {
if (rawValue instanceof List<?>) {
List<?> list = (List<?>) rawValue;
if (list.isEmpty()) {
return null;
} else {
return list.get(0);
}
} else {
return rawValue;
}
}
}