package com.psddev.cms.db; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.codec.language.Metaphone; import com.psddev.dari.db.ComparisonPredicate; import com.psddev.dari.db.CompoundPredicate; import com.psddev.dari.db.ObjectField; import com.psddev.dari.db.ObjectType; import com.psddev.dari.db.Predicate; import com.psddev.dari.db.PredicateParser; import com.psddev.dari.db.Query; import com.psddev.dari.db.Record; import com.psddev.dari.db.Recordable; import com.psddev.dari.util.CollectionUtils; import com.psddev.dari.util.ObjectUtils; @Deprecated public class Search extends Record { private static final Metaphone METAPHONE = new Metaphone(); @Required private String displayName; @Indexed(unique = true) @Required private String internalName; private Set<ObjectType> types; private List<Rule> rules = new ArrayList<Rule>(Arrays.asList(new StopWords())); public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public String getInternalName() { return internalName; } public void setInternalName(String internalName) { this.internalName = internalName; } public Set<ObjectType> getTypes() { if (types == null) { setTypes(new HashSet<ObjectType>()); } return types; } public void setTypes(Set<ObjectType> types) { this.types = types; } public List<Rule> getRules() { if (rules == null) { setRules(new ArrayList<Rule>()); } return rules; } public void setRules(List<Rule> rules) { this.rules = rules; } // --- Fluent methods --- public static Search named(String name) { return Query.from(Search.class).where("internalName = ?", name).first(); } public Search addTypes(ObjectType... types) { if (types != null) { for (ObjectType type : types) { getTypes().add(type); } } return this; } public Search addTypes(Class<?>... classes) { if (classes != null) { for (Class<?> c : classes) { getTypes().add(ObjectType.getInstance(c)); } } return this; } public Search addRule(Rule rule) { getRules().add(rule); return this; } public Search addStopWords(String... stopWords) { if (stopWords != null) { StopWords rule = null; for (Rule r : getRules()) { if (r instanceof StopWords) { rule = (StopWords) r; break; } } if (rule == null) { rule = new StopWords(); addRule(rule); } Collections.addAll(rule.getStopWords(), stopWords); } return this; } public Search boostType(double boost, ObjectType type) { BoostType rule = new BoostType(); rule.setBoost(boost); rule.setType(type); return addRule(rule); } public Search boostType(double boost, Class<?> objectClass) { return boostType(boost, ObjectType.getInstance(objectClass)); } public Search boostLabels(double boost) { if (boost != 1.0) { for (Rule rule : getRules()) { if (rule instanceof BoostLabels) { ((BoostLabels) rule).setBoost(boost); return this; } } BoostLabels rule = new BoostLabels(); rule.setBoost(boost); addRule(rule); } else { for (Iterator<Rule> i = getRules().iterator(); i.hasNext();) { Rule rule = i.next(); if (rule instanceof BoostLabels) { i.remove(); } } } return this; } public Search boostFields(double boost, ObjectType type, String... fields) { BoostFields rule = new BoostFields(); rule.setBoost(boost); rule.setType(type); Collections.addAll(rule.getFields(), fields); return addRule(rule); } public Search boostFields(double boost, Class<?> objectClass, String... fields) { return boostFields(boost, ObjectType.getInstance(objectClass), fields); } public Search boostPhrase(double boost, String pattern, ObjectType type, String predicate) { BoostPhrase rule = new BoostPhrase(); rule.setBoost(boost); rule.setPattern(pattern); rule.setType(type); rule.setPredicate(predicate); return addRule(rule); } public Search boostPhrase(double boost, String pattern, Class<?> objectClass, String predicate) { return boostPhrase(boost, pattern, ObjectType.getInstance(objectClass), predicate); } public Search addTypeKeywords(double boost, ObjectType type, String... keywords) { if (keywords != null) { TypeKeywords rule = new TypeKeywords(); rule.setBoost(boost); rule.setType(type); Collections.addAll(rule.getKeywords(), keywords); addRule(rule); } return this; } public Search addTypeKeywords(double boost, Class<?> objectClass, String... keywords) { return addTypeKeywords(boost, ObjectType.getInstance(objectClass), keywords); } public Search boostDirectoryItems(final double boost) { return addRule(new Rule() { public void apply(Search search, SearchQuery query, List<String> queryTerms) { query.sortRelevant(boost, Directory.Static.hasPathPredicate()); } }); } private List<String> normalizeTerms(Object... terms) { List<String> normalized = new ArrayList<String>(); for (Object term : CollectionUtils.recursiveIterable(terms)) { if (term == null) { continue; } else if (term instanceof Recordable) { normalized.add(((Recordable) term).getState().getId().toString()); } else if (term != null) { String termString = term.toString(); char[] letters = termString.toCharArray(); int lastEnd = 0; for (int i = 0, length = letters.length; i < length; ++ i) { char letter = letters[i]; if (Character.isWhitespace(letter)) { int end = i; for (++ i; i < length && Character.isWhitespace(letters[i]);) { ++ i; } String word = termString.substring(lastEnd, end); lastEnd = i; normalized.add(word); } } normalized.add(termString.substring(lastEnd)); } } return normalized; } public Search addOptionalTerms(double boost, Object... terms) { OptionalTerms rule = new OptionalTerms(); rule.setBoost(boost); rule.setTerms(new HashSet<String>(normalizeTerms(terms))); return addRule(rule); } @Deprecated public SearchQuery toQuery(Object... terms) { List<String> queryTerms = normalizeTerms(terms); SearchQuery query = new SearchQuery(); for (Rule rule : getRules()) { rule.apply(this, query, queryTerms); } if (!queryTerms.isEmpty()) { query.or("_any matchesAll ?", queryTerms); } Set<ObjectType> allTypes = new HashSet<ObjectType>(); for (ObjectType type : getTypes()) { allTypes.addAll(type.as(ToolUi.class).findDisplayTypes()); } query.and("_type = ?", allTypes); return query; } @Deprecated @Embedded public abstract static class Rule extends Record { @Deprecated public abstract void apply(Search search, SearchQuery query, List<String> queryTerms); } public static class StopWords extends Rule { @CollectionMinimum(1) private Set<String> stopWords = new LinkedHashSet<String>(Arrays.asList( "a", "about", "an", "and", "are", "as", "at", "be", "but", "by", "com", "do", "for", "from", "he", "her", "him", "his", "her", "hers", "how", "I", "if", "in", "is", "it", "its", "me", "my", "of", "on", "or", "our", "ours", "that", "the", "they", "this", "to", "too", "us", "she", "was", "what", "when", "where", "who", "will", "with", "why", "www")); public String getLabel() { return "Common words that may be omitted from the search query to provide higher quality results"; } public Set<String> getStopWords() { if (stopWords == null) { setStopWords(new LinkedHashSet<String>()); } return stopWords; } public void setStopWords(Set<String> stopWords) { this.stopWords = stopWords; } @Deprecated @Override public void apply(Search search, SearchQuery query, List<String> queryTerms) { Set<String> stopWords = getStopWords(); Set<String> removed = null; for (Iterator<String> i = queryTerms.iterator(); i.hasNext();) { String word = i.next(); if (stopWords.contains(word)) { i.remove(); if (removed == null) { removed = new HashSet<String>(); } removed.add(word); } } if (removed != null && !removed.isEmpty()) { query.sortRelevant(1.0, "_any matchesAll ?", removed); } } } public abstract static class BoostRule extends Rule { private double boost; public double getBoost() { return boost; } public void setBoost(double boost) { this.boost = boost; } } public static class BoostType extends BoostRule { private ObjectType type; public ObjectType getType() { return type; } public void setType(ObjectType type) { this.type = type; } @Deprecated @Override public void apply(Search search, SearchQuery query, List<String> queryTerms) { query.sortRelevant(getBoost(), "_type = ?", type.as(ToolUi.class).findDisplayTypes()); } } public static class BoostLabels extends BoostRule { @Deprecated @Override public void apply(Search search, SearchQuery query, List<String> queryTerms) { double boost = getBoost(); for (ObjectType type : search.getTypes()) { String prefix = type.getInternalName() + "/"; for (String fieldName : type.getLabelFields()) { query.sortRelevant(boost, prefix + fieldName + " matchesAll ?", queryTerms); } } } } public static class BoostFields extends BoostRule { private ObjectType type; private Set<String> fields; public ObjectType getType() { return type; } public void setType(ObjectType type) { this.type = type; } public Set<String> getFields() { if (fields == null) { setFields(new LinkedHashSet<String>()); } return fields; } public void setFields(Set<String> fields) { this.fields = fields; } @Deprecated @Override public void apply(Search search, SearchQuery query, List<String> queryTerms) { double boost = getBoost(); String prefix = getType().getInternalName() + "/"; for (String field : getFields()) { List<UUID> uuids = new ArrayList<UUID>(); List<String> texts = new ArrayList<String>(); if (queryTerms != null) { for (String queryTerm : queryTerms) { UUID uuid = ObjectUtils.to(UUID.class, queryTerm); if (uuid != null) { uuids.add(uuid); } else { texts.add(queryTerm); } } } if (ObjectField.RECORD_TYPE.equals(type.getField(field).getInternalItemType())) { if (!uuids.isEmpty()) { query.sortRelevant(boost, prefix + field + " matchesAll ?", uuids); } } else { if (!texts.isEmpty()) { query.sortRelevant(boost, prefix + field + " matchesAll ?", texts); } } } } } public static class BoostPhrase extends BoostRule { public String pattern; public ObjectType type; public String predicate; public String getPattern() { return pattern; } public void setPattern(String pattern) { this.pattern = pattern; } public ObjectType getType() { return type; } public void setType(ObjectType type) { this.type = type; } public String getPredicate() { return predicate; } public void setPredicate(String predicate) { this.predicate = predicate; } @Deprecated @Override public void apply(Search search, SearchQuery query, List<String> queryTerms) { StringBuilder queryTermsString = new StringBuilder(); for (String term : queryTerms) { queryTermsString.append(term); queryTermsString.append(' '); } Pattern pattern = Pattern.compile(getPattern(), Pattern.CANON_EQ | Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); Matcher matcher = pattern.matcher(queryTermsString.toString()); while (matcher.find()) { int groupCount = matcher.groupCount(); Object[] parameters = new Object[groupCount]; for (int i = 0; i < groupCount; ++ i) { parameters[i] = matcher.group(i + 1); } Predicate predicate = PredicateParser.Static.parse(getPredicate(), parameters); predicate = addPrefix(getType().getInternalName() + "/", predicate); query.sortRelevant(getBoost(), predicate); } } private Predicate addPrefix(String prefix, Predicate predicate) { if (predicate instanceof CompoundPredicate) { CompoundPredicate compound = (CompoundPredicate) predicate; List<Predicate> children = new ArrayList<Predicate>(); for (Predicate child : compound.getChildren()) { children.add(addPrefix(prefix, child)); } return new CompoundPredicate(compound.getOperator(), children); } else if (predicate instanceof ComparisonPredicate) { ComparisonPredicate comparison = (ComparisonPredicate) predicate; return new ComparisonPredicate( comparison.getOperator(), comparison.isIgnoreCase(), prefix + comparison.getKey(), comparison.getValues()); } else { return predicate; } } } public static class TypeKeywords extends BoostRule { private ObjectType type; private Set<String> keywords; public ObjectType getType() { return type; } public void setType(ObjectType type) { this.type = type; } public Set<String> getKeywords() { if (keywords == null) { setKeywords(new LinkedHashSet<String>()); } return keywords; } public void setKeywords(Set<String> keywords) { this.keywords = keywords; } @Deprecated @Override public void apply(Search search, SearchQuery query, List<String> queryTerms) { List<ObjectType> types = getType().as(ToolUi.class).findDisplayTypes(); for (Iterator<String> i = queryTerms.iterator(); i.hasNext();) { String word = i.next(); String similar = findSimilar(word); if (similar != null) { i.remove(); query.and("_type = ? or _any matchesAll ?", types, word); query.sortRelevant(getBoost(), "_type = ?", types); if (!similar.equalsIgnoreCase(word)) { query.getSubstitutions().put(word, similar); } } } } private String findSimilar(String term) { ObjectType type = getType(); String encodedTerm = METAPHONE.encode(term); String displayName = type.getDisplayName(); if (encodedTerm.equals(METAPHONE.encode(displayName))) { return displayName; } for (String keyword : getKeywords()) { if (encodedTerm.equals(METAPHONE.encode(keyword))) { return keyword; } } return null; } } public static class OptionalTerms extends BoostRule { private Set<String> terms; public Set<String> getTerms() { if (terms == null) { terms = new HashSet<String>(); } return terms; } public void setTerms(Set<String> terms) { this.terms = terms; } @Deprecated @Override public void apply(Search search, SearchQuery query, List<String> queryTerms) { Set<String> terms = getTerms(); for (Iterator<String> i = queryTerms.iterator(); i.hasNext();) { String queryTerm = i.next(); if (terms.contains(queryTerm)) { i.remove(); } } query.or("_any matchesAny ?", terms); query.sortRelevant(getBoost(), "_any matchesAny ?", terms); } } }