1 // Copyright 2010 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.api
.search
;
5 import com
.google
.appengine
.api
.search
.checkers
.DocumentChecker
;
6 import com
.google
.appengine
.api
.search
.checkers
.FieldChecker
;
7 import com
.google
.appengine
.api
.search
.checkers
.Preconditions
;
8 import com
.google
.apphosting
.api
.search
.DocumentPb
;
9 import com
.google
.common
.collect
.HashMultimap
;
10 import com
.google
.common
.collect
.SetMultimap
;
12 import java
.io
.IOException
;
13 import java
.io
.ObjectInputStream
;
14 import java
.io
.Serializable
;
15 import java
.util
.ArrayList
;
16 import java
.util
.Collections
;
17 import java
.util
.HashMap
;
18 import java
.util
.LinkedHashMap
;
19 import java
.util
.List
;
20 import java
.util
.Locale
;
25 * Represents a user generated document. The following example shows how to
26 * create a document consisting of a set of fields, some with plain text and
27 * some in HTML; it also adds facets to the document.
29 * Document document = Document.newBuilder().setId("document id")
30 * .setLocale(Locale.UK)
31 * .addField(Field.newBuilder()
33 * .setText("going for dinner"))
34 * .addField(Field.newBuilder()
36 * .setHTML("<html>I found a restaurant.</html>"))
37 * .addField(Field.newBuilder()
38 * .setName("signature")
39 * .setText("ten post jest przeznaczony dla odbiorcy")
40 * .setLocale(new Locale("pl")))
41 * .addFacet(Facet.withAtom("tag", "food"))
42 * .addFacet(Facet.withNumber("priority", 5.0))
46 * The following example shows how to access the fields within a document:
48 * Document document = ...
50 * for (Field field : document.getFields()) {
51 * switch (field.getType()) {
52 * case TEXT: use(field.getText()); break;
53 * case HTML: use(field.getHtml()); break;
54 * case ATOM: use(field.getAtom()); break;
55 * case DATE: use(field.getDate()); break;
60 * And this example shows how to access the facets within a document:
62 * Document document = ...
64 * for (Facet facet : document.getFacets()) {
65 * switch (facet.getType()) {
66 * case ATOM: use(facet.getAtom()); break;
67 * case NUMBER: use(facet.getNumber()); break;
72 public class Document
implements Serializable
{
74 static final int MAX_FIELDS_TO_STRING
= 10;
75 static final int MAX_FACETS_TO_STRING
= 10;
78 * A builder of documents. This is not thread-safe.
80 public static class Builder
{
81 private final Map
<String
, List
<Field
>> fieldMap
= new HashMap
<>();
82 private final List
<Field
> fields
= new ArrayList
<>();
83 private final List
<Facet
> facets
= new ArrayList
<>();
85 private final SetMultimap
<String
, Field
.FieldType
> noRepeatNames
= HashMultimap
.create();
86 private String documentId
; private Locale locale
; private Integer rank
;
89 * Constructs a builder for a document.
95 * Set the document id to a unique valid value. A valid document id must
96 * be a printable ASCII string of between 1 and
97 * {@literal DocumentChecker#MAXIMUM_DOCUMENT_ID_LENGTH} characters, and
98 * also not start with '!' which is reserved. If no document id is
99 * provided, then the search service will provide one when the document
102 * @param documentId the unique id for the document to be built
103 * @return this builder
104 * @throws IllegalArgumentException if documentId is not valid
106 public Builder
setId(String documentId
) {
107 if (documentId
!= null) {
108 this.documentId
= DocumentChecker
.checkDocumentId(documentId
);
114 * Adds the field builder to the document builder. Allows multiple
115 * fields with the same name.
117 * @param builder the builder of the field to add
118 * @return this document builder
120 public Builder
addField(Field
.Builder builder
) {
121 Preconditions
.checkNotNull(builder
, "field builder cannot be null");
122 return addField(builder
.build());
126 * Adds the field to the builder. Allows multiple fields with the same name, except
127 * that documents may only have one date and one number field for a name.
129 * @param field the field to add
130 * @return this builder
131 * @throws IllegalArgumentException if the field is invalid
133 public Builder
addField(Field field
) {
134 Preconditions
.checkNotNull(field
, "field cannot be null");
135 Field
.FieldType type
= field
.getType();
136 if (type
== Field
.FieldType
.DATE
|| type
== Field
.FieldType
.NUMBER
) {
137 if (!noRepeatNames
.put(field
.getName(), type
)) {
138 throw new IllegalArgumentException("Number and date fields cannot be repeated.");
143 List
<Field
> fieldsForName
= fieldMap
.get(field
.getName());
144 if (fieldsForName
== null) {
145 fieldsForName
= new ArrayList
<>();
146 fieldMap
.put(field
.getName(), fieldsForName
);
148 fieldsForName
.add(field
);
153 * Adds a {@link Facet} to this builder.
155 * @param facet the facet to add
156 * @return this builder
158 public Builder
addFacet(Facet facet
) {
159 Preconditions
.checkNotNull(facet
, "facet cannot be null");
165 * Sets the {@link Locale} the document is written in.
167 * @param locale the {@link Locale} the document is written in
168 * @return this document builder
170 public Builder
setLocale(Locale locale
) {
171 this.locale
= locale
;
176 * Sets the rank of this document, which determines the order of documents
177 * returned by search, if no sorting or scoring is given. If it is not
178 * specified, then the number of seconds since 2011/1/1 will be used.
180 * @param rank the rank of this document
181 * @return this builder
183 public Builder
setRank(int rank
) {
189 * Builds a valid document. The builder must have set a valid document
190 * id, and have a non-empty set of valid fields.
192 * @return the document built by this builder
193 * @throws IllegalArgumentException if the document built is not valid
195 public Document
build() {
196 return new Document(this);
200 private static final long serialVersionUID
= 309382038422977263L;
202 private final String documentId
;
203 private final Map
<String
, List
<Field
>> fieldMap
;
204 private final List
<Field
> fields
;
205 private volatile List
<Facet
> facets
;
206 private transient volatile Map
<String
, List
<Facet
>> facetMap
;
208 private final int rank
;
210 private final Locale locale
;
213 * Constructs a document with the given builder.
215 * @param builder the builder capable of building a document
217 protected Document(Builder builder
) {
218 documentId
= builder
.documentId
;
219 fieldMap
= new HashMap
<>(builder
.fieldMap
);
220 fields
= Collections
.unmodifiableList(builder
.fields
);
221 facets
= Collections
.unmodifiableList(builder
.facets
);
222 facetMap
= buildFacetMap(facets
);
223 locale
= builder
.locale
;
224 rank
= Util
.defaultIfNull(builder
.rank
, DocumentChecker
.getNumberOfSecondsSince());
229 * Returns an iterable of {@link Field} in the document
231 public Iterable
<Field
> getFields() {
236 * Returns an iterable of {@link Facet} in the document
238 public Iterable
<Facet
> getFacets() {
243 * Returns an unmodifiable {@link Set} of the field names in the document
245 public Set
<String
> getFieldNames() {
246 return Collections
.unmodifiableSet(fieldMap
.keySet());
250 * Returns an unmodifiable {@link Set} of the facet names in the document
252 public Set
<String
> getFacetNames() {
253 return facetMap
.keySet();
257 * Returns an iterable of all fields with the given name.
259 * @param name the name of the field whose values are to be returned
260 * @return an unmodifiable {@link Iterable} of {@link Field} with the given name
263 public Iterable
<Field
> getFields(String name
) {
264 List
<Field
> fieldsForName
= fieldMap
.get(name
);
265 if (fieldsForName
== null) {
268 return Collections
.unmodifiableList(fieldsForName
);
272 * Returns an iterable of all facets with the given name.
274 * @param name the name of the facet whose values are to be returned
275 * @return an unmodifiable {@link Iterable} of {@link Facet} with the given name
278 public Iterable
<Facet
> getFacets(String name
) {
279 List
<Facet
> facetsForName
= facetMap
.get(name
);
280 if (facetsForName
== null) {
283 return Collections
.unmodifiableList(facetsForName
);
287 * Returns the single field with the given name.
289 * @param name the name of the field to return
290 * @return the single field with name
291 * @throws IllegalArgumentException if the document does not have exactly
292 * one field with the name
294 public Field
getOnlyField(String name
) {
295 List
<Field
> fieldsForName
= fieldMap
.get(name
);
296 Preconditions
.checkArgument(
297 fieldsForName
!= null && fieldsForName
.size() == 1,
298 "Field %s is present %d times; expected 1",
299 name
, (fieldsForName
== null ?
0 : fieldsForName
.size()));
300 return fieldsForName
.get(0);
304 * Returns the single facet with the given name.
306 * @param name the name of the facet to return
307 * @return the single facet with name
308 * @throws IllegalArgumentException if the document does not have exactly
309 * one facet with the name
311 public Facet
getOnlyFacet(String name
) {
312 List
<Facet
> facetsForName
= facetMap
.get(name
);
313 Preconditions
.checkArgument(
314 facetsForName
!= null && facetsForName
.size() == 1,
315 "Facet %s is present %d times; expected 1",
316 name
, (facetsForName
== null ?
0 : facetsForName
.size()));
317 return facetsForName
.get(0);
321 * Returns the number of times a field with the given name is present
324 * @param name the name of the field to be counted
325 * @return the number of times a field with the given name is present
327 public int getFieldCount(String name
) {
328 List
<Field
> fieldsForName
= fieldMap
.get(name
);
329 return fieldsForName
== null ?
0 : fieldsForName
.size();
333 * Returns the number of times a facet with the given name is present
336 * @param name the name of the facet to be counted
337 * @return the number of times a facet with the given name is present
339 public int getFacetCount(String name
) {
340 List
<Facet
> facetsForName
= facetMap
.get(name
);
341 return facetsForName
== null ?
0 : facetsForName
.size();
345 * @return the id of the document
347 public String
getId() {
352 * @return the {@link Locale} the document is written in. Can be null
354 public Locale
getLocale() {
359 * Returns the rank of this document. A document's rank is used to
360 * determine the default order in which documents are returned by
361 * search, if no sorting or scoring is specified.
363 * @return the rank of this document
365 public int getRank() {
370 public int hashCode() {
371 return documentId
.hashCode();
375 public boolean equals(Object object
) {
376 if (this == object
) {
379 if (!(object
instanceof Document
)) {
382 Document doc
= (Document
) object
;
383 return documentId
.equals(doc
.getId());
387 * Checks whether the document is valid. A document is valid if
388 * it has a valid document id, a locale, a non-empty collection of
391 * @return this document
392 * @throws IllegalArgumentException if the document has an invalid
393 * document id, has no fields, or some field is invalid
395 private Document
checkValid() {
396 if (documentId
!= null) {
397 DocumentChecker
.checkDocumentId(documentId
);
399 Preconditions
.checkArgument(fieldMap
!= null,
400 "Null map of fields in document for indexing");
401 Preconditions
.checkArgument(fields
!= null,
402 "Null list of fields in document for indexing");
403 Preconditions
.checkArgument(facetMap
!= null,
404 "Null map of facets in document for indexing");
405 Preconditions
.checkArgument(facets
!= null,
406 "Null list of facets in document for indexing");
410 private static Map
<String
, List
<Facet
>> buildFacetMap(List
<Facet
> facets
) {
411 Map
<String
, List
<Facet
>> facetMap
= new LinkedHashMap
<>();
412 for (Facet facet
: facets
) {
413 List
<Facet
> facetsForName
= facetMap
.get(facet
.getName());
414 if (facetsForName
== null) {
415 facetsForName
= new ArrayList
<>();
416 facetMap
.put(facet
.getName(), facetsForName
);
418 facetsForName
.add(facet
);
420 return Collections
.unmodifiableMap(facetMap
);
423 private void readObject(ObjectInputStream in
) throws IOException
, ClassNotFoundException
{
424 in
.defaultReadObject();
425 if (facets
== null) {
426 facets
= Collections
.emptyList();
428 facetMap
= buildFacetMap(facets
);
432 * Creates a new document builder. You must use this method to obtain a new
433 * builder. The returned builder must be used to specify all properties of
434 * the document. To obtain the document call the {@link Builder#build()}
435 * method on the returned builder.
437 * @return a builder which constructs a document object
439 public static Builder
newBuilder() {
440 return new Builder();
444 * Creates a new document builder from the given document. All document
445 * properties are copied to the returned builder.
447 * @param document the document protocol buffer to build a document object
449 * @return the document builder initialized from a document protocol buffer
450 * @throws SearchException if a field or facet value is invalid
452 static Builder
newBuilder(DocumentPb
.Document document
) {
453 Document
.Builder docBuilder
= Document
.newBuilder().setId(document
.getId());
454 if (document
.hasLanguage()) {
455 docBuilder
.setLocale(FieldChecker
.parseLocale(document
.getLanguage()));
457 for (DocumentPb
.Field field
: document
.getFieldList()) {
458 docBuilder
.addField(Field
.newBuilder(field
));
460 for (DocumentPb
.Facet facet
: document
.getFacetList()) {
461 docBuilder
.addFacet(Facet
.withProtoMessage(facet
));
463 if (document
.hasOrderId()) {
464 docBuilder
.setRank(document
.getOrderId());
470 * Copies a {@link Document} object into a
471 * {@link com.google.apphosting.api.search.DocumentPb.Document} protocol buffer.
473 * @return the document protocol buffer copy of the document object
474 * @throws IllegalArgumentException if any parts of the document are invalid
475 * or the document protocol buffer is too large
477 DocumentPb
.Document
copyToProtocolBuffer() {
478 DocumentPb
.Document
.Builder docBuilder
= DocumentPb
.Document
.newBuilder();
479 if (documentId
!= null) {
480 docBuilder
.setId(documentId
);
482 if (locale
!= null) {
483 docBuilder
.setLanguage(locale
.toString());
485 for (Field field
: fields
) {
486 docBuilder
.addField(field
.copyToProtocolBuffer());
488 for (Facet facet
: getFacets()) {
489 docBuilder
.addFacet(facet
.copyToProtocolBuffer());
491 docBuilder
.setOrderId(rank
);
492 return DocumentChecker
.checkValid(docBuilder
.build());
496 public String
toString() {
497 return new Util
.ToStringHelper("Document")
498 .addField("documentId", documentId
)
499 .addIterableField("fields", fields
, MAX_FIELDS_TO_STRING
)
500 .addIterableField("facets", getFacets(), MAX_FACETS_TO_STRING
)
501 .addField("locale", locale
)
502 .addField("rank", rank
)
506 boolean isIdenticalTo(Document other
) {
507 if (documentId
== null) {
508 if (other
.documentId
!= null) {
512 if (!documentId
.equals(other
.documentId
)) {
516 if (fields
== null) {
517 if (other
.fields
!= null) {
521 if (!fields
.equals(other
.fields
)) {
525 if (!getFacets().equals(other
.getFacets())) {
528 if (locale
== null) {
529 if (other
.locale
!= null) {
533 if (!locale
.equals(other
.locale
)) {
537 return rank
== other
.rank
;