1 package com
.google
.appengine
.api
.search
;
3 import com
.google
.appengine
.api
.search
.checkers
.FacetChecker
;
4 import com
.google
.appengine
.api
.search
.checkers
.Preconditions
;
5 import com
.google
.apphosting
.api
.search
.DocumentPb
;
6 import com
.google
.apphosting
.api
.search
.DocumentPb
.FacetValue
;
7 import com
.google
.apphosting
.api
.search
.DocumentPb
.FacetValue
.ContentType
;
9 import java
.io
.Serializable
;
10 import java
.util
.Objects
;
13 * A {@code Facet} can be used to categorize a {@link Document}. It is not a {@link Field}.
15 * Search results can contain facets for the extended result set and their value frequency.
16 * For example, if a search query is related to "wine", then facets could be "color" with values
17 * of "red" and "white", and "year" with values of "2000" and "2005".
19 * Each facet has a name and exactly one value: atom or number. Facet name lengths are between 1 and
20 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_NAME_LENGTH} characters,
22 * limited to {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_ATOM_LENGTH}
23 * characters. Numbers must be between
24 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MINIMUM_NUMBER_VALUE} and
25 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_NUMBER_VALUE}.
27 public final class Facet
implements Serializable
{
30 * Creates and returns an atom facet with the given {@code name} and {@code value}.
32 * @return an instance of {@link Facet}.
33 * @throws IllegalArgumentException if the facet name or value are invalid.
35 public static Facet
withAtom(String name
, String value
) {
36 return new Facet(name
, value
, null);
40 * Creates and returns a number facet with the given {@code name} and {@code value}.
42 * @return an instance of {@link Facet}.
43 * @throws IllegalArgumentException if the facet name or value are invalid.
45 public static Facet
withNumber(String name
, Double value
) {
46 return new Facet(name
, null, value
);
49 private static final long serialVersionUID
= 3297968709406212335L;
51 private final String name
;
53 private Double number
;
55 private Facet(String name
, String atom
, Double number
) {
63 * Returns the name of the facet.
65 public String
getName() {
70 * Returns the atomic value of the facet. Returns null if the value is not atomic.
72 public String
getAtom() {
77 * Returns the numeric value of the facet. Returns null if the value is not numeric.
79 public Double
getNumber() {
84 public int hashCode() {
85 return Objects
.hash(name
, atom
, number
);
89 public boolean equals(Object object
) {
93 if (!(object
instanceof Facet
)) {
96 Facet facet
= (Facet
) object
;
97 return Util
.equalObjects(name
, facet
.name
)
98 && Util
.equalObjects(atom
, facet
.atom
)
99 && Util
.equalObjects(number
, facet
.number
);
103 * Checks whether the facet is valid, specifically,
104 * whether the facet name, value are valid.
105 * Also that at most one value: atom or number is set.
107 * @throws IllegalArgumentException if facet name, atom, or number is invalid
109 private void checkValid() {
110 FacetChecker
.checkFacetName(name
);
112 if (number
!= null) {
113 throw new IllegalArgumentException(
114 String
.format("both atom and number are set for facet %s", name
));
116 FacetChecker
.checkAtom(atom
);
117 } else if (number
!= null) {
118 FacetChecker
.checkNumber(number
);
120 throw new IllegalArgumentException(
121 String
.format("neither atom nor number is set for facet %s", name
));
126 * Creates and returns a facet reflecting a {@link DocumentPb.Facet} protocol message.
128 * @throws SearchException if the facet contains an invalid name, atom or number.
130 static Facet
withProtoMessage(DocumentPb
.Facet facet
) {
131 FacetValue value
= facet
.getValue();
132 switch (value
.getType()) {
135 return withAtom(facet
.getName(), value
.getStringValue());
136 } catch (IllegalArgumentException e
) {
137 throw new SearchException(
138 String
.format("Failed to create facet %s from protocol message: %s", facet
.getName(),
143 return withNumber(facet
.getName(), Facet
.stringToNumber(value
.getStringValue()));
144 } catch (NumberFormatException e
) {
145 throw new SearchException("Failed to parse double: " + value
.getStringValue());
146 } catch (IllegalArgumentException e
) {
147 throw new SearchException(
148 String
.format("Failed to create facet %s from protocol message: %s", facet
.getName(),
152 throw new SearchException(
153 String
.format("unknown facet type %s for facet %s",
154 String
.valueOf(value
.getType()), facet
.getName()));
159 * Copies a {@link Facet} object into a {@link com.google.apphosting.api.search.DocumentPb.Facet}
162 * @return the facet protocol buffer copy of this facet object
164 DocumentPb
.Facet
copyToProtocolBuffer() {
165 DocumentPb
.FacetValue
.Builder facetValueBuilder
= DocumentPb
.FacetValue
.newBuilder();
167 facetValueBuilder
.setType(ContentType
.ATOM
);
168 facetValueBuilder
.setStringValue(atom
);
169 } else if (number
!= null) {
170 facetValueBuilder
.setType(ContentType
.NUMBER
);
171 facetValueBuilder
.setStringValue(numberToString(number
));
173 DocumentPb
.Facet
.Builder builder
= DocumentPb
.Facet
.newBuilder()
175 .setValue(facetValueBuilder
);
176 return builder
.build();
180 public String
toString() {
181 return new Util
.ToStringHelper("Facet")
182 .addField("name", name
)
183 .addField("atom", atom
)
184 .addField("number", number
)
189 * Converts a double number into a string acceptable by faceting backend as a number.
191 * @throws IllegalArgumentException if the {@code number} is NaN.
193 static String
numberToString(double number
) {
194 Preconditions
.checkArgument(!Double
.isNaN(number
), "should be a number.");
195 return Double
.toString(number
);
199 * Converts a string with faceting backend format into double number.
201 * @throws NullPointerException if the string is null
202 * @throws NumberFormatException if the string does not contain a parsable double.
204 static Double
stringToNumber(String string
) {
205 return Double
.parseDouble(string
);