1 // Copyright 2010 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.api
.search
;
5 import com
.google
.appengine
.api
.search
.SearchServicePb
.SearchParams
;
6 import com
.google
.appengine
.api
.search
.checkers
.Preconditions
;
7 import com
.google
.appengine
.api
.search
.checkers
.QueryOptionsChecker
;
8 import com
.google
.appengine
.api
.search
.checkers
.SearchApiLimits
;
9 import com
.google
.common
.collect
.ImmutableList
;
11 import java
.util
.ArrayList
;
12 import java
.util
.Arrays
;
13 import java
.util
.List
;
16 * Represents options which control where and what in the search results to
17 * return, from restricting the document fields returned to those given, and
18 * scoring and sorting the results, whilst supporting pagination.
20 * For example, the following options will return documents from search
21 * results for some given query, returning up to 20 results including the
22 * fields 'author' and 'date-sent' as well as snippeted fields 'subject' and
23 * 'body'. The results are sorted by 'author' in descending order, getting
24 * the next 20 results from the responseCursor in the previously returned
25 * results, giving back a single cursor in the {@link Results} to
26 * get the next batch of results after this.
29 * QueryOptions request = QueryOptions.newBuilder()
31 * .setFieldsToReturn("author", "date-sent")
32 * .setFieldsToSnippet("subject", "body")
33 * .setSortOptions(SortOptions.newBuilder().
34 * .addSortExpression(SortExpression.newBuilder()
35 * .setExpression("author")
36 * .setDirection(Scorer.SortDirection.DESCENDING)
37 * .setDefaultValue("")))
39 * .setCursor(Cursor.newBuilder().build())
43 public final class QueryOptions
{
46 * A builder which constructs QueryOptions objects.
48 public static final class Builder
{
49 private Integer limit
;
51 private ImmutableList
<String
> fieldsToReturn
= ImmutableList
.of();
52 private ImmutableList
<String
> fieldsToSnippet
= ImmutableList
.of();
53 private List
<FieldExpression
> expressionsToReturn
= new ArrayList
<FieldExpression
>();
54 private SortOptions sortOptions
; private Cursor cursor
; private Integer numberFoundAccuracy
; private Integer offset
; private Boolean idsOnly
;
60 * Constructs a {@link QueryOptions} builder with the given request.
62 * @param request the search request to populate the builder
64 private Builder(QueryOptions request
) {
65 limit
= request
.getLimit();
66 cursor
= request
.getCursor();
67 numberFoundAccuracy
= request
.getNumberFoundAccuracy();
68 sortOptions
= request
.getSortOptions();
69 fieldsToReturn
= ImmutableList
.copyOf(request
.getFieldsToReturn());
70 fieldsToSnippet
= ImmutableList
.copyOf(request
.getFieldsToSnippet());
71 expressionsToReturn
= new ArrayList
<FieldExpression
>(request
.getExpressionsToReturn());
75 * Sets the limit on the number of documents to return in {@link Results}.
77 * @param limit the number of documents to return
78 * @return this Builder
79 * @throws IllegalArgumentException if numDocumentsToReturn is
80 * not within acceptable range
82 public Builder
setLimit(int limit
) {
83 this.limit
= QueryOptionsChecker
.checkLimit(limit
);
88 * Sets the cursor. The cursor is obtained from either a
89 * {@link Results} or one of the individual
90 * {@link ScoredDocument ScoredDocuments}.
92 * This is illustrated from the following code fragment:
95 * Cursor cursor = Cursor.newBuilder().build();
97 * SearchResults results = index.search(
99 * .setOptions(QueryOptions.newBuilder()
103 * .build("some query"));
105 * // If the Cursor is built without setPerResult(true), then
106 * // by default a single {@link Cursor} is returned with the
107 * // {@link Results}.
108 * cursor = results.getCursor();
110 * for (ScoredDocument result : results) {
111 * // If you set Cursor.newBuilder().setPerResult(true)
112 * // then a cursor is returned with each result.
113 * result.getCursor();
116 * @param cursor use a cursor returned from a
117 * previous set of search results as a starting point to retrieve
118 * the next set of results. This can get you better performance, and
119 * also improves the consistency of pagination through index updates
120 * @return this Builder
122 public Builder
setCursor(Cursor cursor
) {
123 Preconditions
.checkArgument(offset
== null || cursor
== null,
124 "offset and cursor cannot be set in the same request");
125 this.cursor
= cursor
;
130 * Sets a cursor built from the builder.
132 * @see #setCursor(Cursor)
133 * @param cursorBuilder a {@link Cursor.Builder} that is used to build
135 * @return this Builder
137 public Builder
setCursor(Cursor
.Builder cursorBuilder
) {
138 return setCursor(cursorBuilder
.build());
142 * Sets the offset of the first result to return.
144 * @param offset the offset into all search results to return the limit
146 * @return this Builder
147 * @throws IllegalArgumentException if the offset is negative or is larger
148 * than {@link SearchApiLimits#SEARCH_MAXIMUM_OFFSET}
150 public Builder
setOffset(int offset
) {
151 Preconditions
.checkArgument(cursor
== null,
152 "offset and cursor cannot be set in the same request");
153 this.offset
= QueryOptionsChecker
.checkOffset(offset
);
158 * Sets the accuracy requirement for
159 * {@link Results#getNumberFound()}. If set,
160 * {@code getNumberFound()} will be accurate up to at least that number.
161 * For example, when set to 100, any {@code getNumberFound()} <= 100 is
162 * accurate. This option may add considerable latency / expense, especially
163 * when used with {@link Builder#setFieldsToReturn(String...)}.
165 * @param numberFoundAccuracy the minimum accuracy requirement
166 * @return this Builder
167 * @throws IllegalArgumentException if the accuracy is not within
170 public Builder
setNumberFoundAccuracy(int numberFoundAccuracy
) {
171 this.numberFoundAccuracy
=
172 QueryOptionsChecker
.checkNumberFoundAccuracy(numberFoundAccuracy
);
177 * Clears any accuracy requirement for {@link Results#getNumberFound()}.
179 public Builder
clearNumberFoundAccuracy() {
180 this.numberFoundAccuracy
= SearchApiLimits
.SEARCH_DEFAULT_NUMBER_FOUND_ACCURACY
;
185 * Specifies one or more fields to return in results.
187 * @param fields the names of fields to return in results
188 * @return this Builder
189 * @throws IllegalArgumentException if any of the field names is invalid
191 public Builder
setFieldsToReturn(String
... fields
) {
192 Preconditions
.checkNotNull(fields
, "field names cannot be null");
193 Preconditions
.checkArgument(idsOnly
== null,
194 "You may not set fields to return if search returns keys only");
195 this.fieldsToReturn
= ImmutableList
.copyOf(
196 QueryOptionsChecker
.checkFieldNames(Arrays
.asList(fields
)));
201 * Specifies one or more fields to snippet in results. Snippets will be
202 * returned as fields with the same names in
203 * {@link ScoredDocument#getExpressions()}.
205 * @param fieldsToSnippet the names of fields to snippet in results
206 * @return this Builder
207 * @throws IllegalArgumentException if any of the field names is invalid
209 public Builder
setFieldsToSnippet(String
... fieldsToSnippet
) {
210 Preconditions
.checkNotNull(fieldsToSnippet
, "field names cannot be null");
211 this.fieldsToSnippet
= ImmutableList
.copyOf(
212 QueryOptionsChecker
.checkFieldNames(Arrays
.asList(fieldsToSnippet
)));
217 * Adds a {@link FieldExpression} build from the given
218 * {@code expressionBuilder} to return in search results. Snippets will be
219 * returned as fields with the same names in
220 * {@link ScoredDocument#getExpressions()}.
222 * @param expressionBuilder a builder of named expressions to
223 * evaluate and return in results
224 * @return this Builder
226 public Builder
addExpressionToReturn(FieldExpression
.Builder expressionBuilder
) {
227 Preconditions
.checkArgument(idsOnly
== null,
228 "You may not add expressions to return if search returns keys only");
229 return addExpressionToReturn(expressionBuilder
.build());
233 * Sets whether or not the search should return documents or document IDs only.
234 * This setting is incompatible with
235 * {@link #addExpressionToReturn(FieldExpression)} and with
236 * {@link #setFieldsToReturn(String...)} methods.
238 * @param idsOnly whether or not only IDs of documents are returned by search request
239 * @return this Builder
241 public Builder
setReturningIdsOnly(boolean idsOnly
) {
242 Preconditions
.checkArgument(expressionsToReturn
.isEmpty(),
243 "You cannot request IDs only if expressions to return are set");
244 Preconditions
.checkArgument(fieldsToReturn
.isEmpty(),
245 "You cannot request IDs only if fields to return are already set");
246 this.idsOnly
= idsOnly
;
251 * Adds a {@link FieldExpression} to return in search results.
253 * @param expression a named expression to compute and return in results
254 * @return this Builder
256 public Builder
addExpressionToReturn(FieldExpression expression
) {
257 this.expressionsToReturn
.add(expression
);
262 * Sets a {@link SortOptions} to sort documents with.
264 * @param sortOptions specifies how to sort the documents in {@link Results}
265 * @return this Builder
267 public Builder
setSortOptions(SortOptions sortOptions
) {
268 this.sortOptions
= sortOptions
;
273 * Sets a {@link SortOptions} using a builder.
275 * @param builder a builder of a {@link SortOptions}
276 * @return this Builder
278 public Builder
setSortOptions(SortOptions
.Builder builder
) {
279 this.sortOptions
= builder
.build();
284 * Construct the final message.
286 * @return the QueryOptions built from the parameters entered on this
288 * @throws IllegalArgumentException if the search request is invalid
290 public QueryOptions
build() {
291 return new QueryOptions(this);
295 private final int limit
;
297 private final int numberFoundAccuracy
;
299 private final ImmutableList
<String
> fieldsToReturn
;
300 private final ImmutableList
<String
> fieldsToSnippet
;
301 private final ImmutableList
<FieldExpression
> expressionsToReturn
;
302 private final SortOptions sortOptions
; private final Cursor cursor
; private final Integer offset
; private final Boolean idsOnly
;
305 * Creates a search request from the builder.
307 * @param builder the search request builder to populate with
309 private QueryOptions(Builder builder
) {
310 limit
= QueryOptionsChecker
.checkLimit(
311 Util
.defaultIfNull(builder
.limit
, SearchApiLimits
.SEARCH_DEFAULT_LIMIT
));
312 numberFoundAccuracy
= Util
.defaultIfNull(builder
.numberFoundAccuracy
,
313 SearchApiLimits
.SEARCH_DEFAULT_NUMBER_FOUND_ACCURACY
);
314 sortOptions
= builder
.sortOptions
;
315 cursor
= builder
.cursor
;
316 offset
= QueryOptionsChecker
.checkOffset(builder
.offset
);
318 fieldsToReturn
= builder
.fieldsToReturn
;
319 fieldsToSnippet
= builder
.fieldsToSnippet
;
320 expressionsToReturn
= ImmutableList
.copyOf(builder
.expressionsToReturn
);
321 idsOnly
= builder
.idsOnly
;
326 * @return the limit on the number of documents to return in search
329 public int getLimit() {
334 * @return a cursor returned from a previous set of
335 * search results to use as a starting point to retrieve the next
336 * set of results. Can be null
338 public Cursor
getCursor() {
343 * @return the offset of the first result to return; returns 0 if
346 public int getOffset() {
347 return (offset
== null) ?
0 : offset
.intValue();
351 * Returns true iff there is an accuracy requirement set.
353 * @return the found count accuracy
355 public boolean hasNumberFoundAccuracy() {
356 return numberFoundAccuracy
!= SearchApiLimits
.SEARCH_DEFAULT_NUMBER_FOUND_ACCURACY
;
360 * Any {@link Results#getNumberFound()} less than or equal to this
361 * setting will be accurate.
363 * @return the found count accuracy
365 public int getNumberFoundAccuracy() {
366 return numberFoundAccuracy
;
370 * @return a {@link SortOptions} specifying how to sort Documents in
373 public SortOptions
getSortOptions() {
378 * @return if this search request returns results document IDs only
380 public boolean isReturningIdsOnly() {
381 return idsOnly
== null ?
false : idsOnly
.booleanValue();
385 * @return an unmodifiable list of names of fields to return in search
388 public List
<String
> getFieldsToReturn() {
389 return fieldsToReturn
;
393 * @return an unmodifiable list of names of fields to snippet in search
396 public List
<String
> getFieldsToSnippet() {
397 return fieldsToSnippet
;
401 * @return an unmodifiable list of expressions which will be evaluated
402 * and returned in results
404 public List
<FieldExpression
> getExpressionsToReturn() {
405 return expressionsToReturn
;
409 * Creates and returns a {@link QueryOptions} builder. Set the search request
410 * parameters and use the {@link Builder#build()} method to create a concrete
411 * instance of QueryOptions.
413 * @return a {@link Builder} which can construct a search request
415 public static Builder
newBuilder() {
416 return new Builder();
420 * Creates a builder from the given request.
422 * @param request the search request for the builder to use
423 * to build another request
424 * @return a new builder with values set from the given request
426 public static Builder
newBuilder(QueryOptions request
) {
427 return new Builder(request
);
431 * Checks the search specification is valid, specifically, has
432 * a non-null number of documents to return specification, a valid
433 * cursor if present, valid sort specification list, a valid
434 * collection of field names for sorting.
436 * @return this checked QueryOptions
437 * @throws IllegalArgumentException if some part of the specification is
440 private QueryOptions
checkValid() {
441 Preconditions
.checkNotNull(limit
, "number of documents to return cannot be null");
442 QueryOptionsChecker
.checkFieldNames(fieldsToReturn
);
447 * Wraps quotes around an escaped argument string.
449 * @param argument the string to escape quotes and wrap with quotes
450 * @return the wrapped and escaped argument string
452 private static String
quoteString(String argument
) {
453 return "\"" + argument
.replace("\"", "\\\"") + "\"";
457 * Copies the contents of this {@link QueryOptions} object into a
458 * {@link SearchParams} protocol buffer builder.
460 * @return a search params protocol buffer builder initialized with
461 * the values from this request
462 * @throws IllegalArgumentException if the cursor type is
465 SearchParams
.Builder
copyToProtocolBuffer(SearchParams
.Builder builder
, String query
) {
466 builder
.setLimit(getLimit());
467 if (cursor
!= null) {
468 cursor
.copyToProtocolBuffer(builder
);
470 builder
.setCursorType(SearchParams
.CursorType
.NONE
);
472 if (offset
!= null) {
473 builder
.setOffset(offset
);
475 if (idsOnly
!= null) {
476 builder
.setKeysOnly(idsOnly
);
478 if (hasNumberFoundAccuracy()) {
479 builder
.setMatchedCountAccuracy(numberFoundAccuracy
);
481 if (sortOptions
!= null) {
482 sortOptions
.copyToProtocolBuffer(builder
);
484 if (!fieldsToReturn
.isEmpty() || !fieldsToSnippet
.isEmpty() || !expressionsToReturn
.isEmpty()) {
485 SearchServicePb
.FieldSpec
.Builder fieldSpec
= SearchServicePb
.FieldSpec
.newBuilder();
486 fieldSpec
.addAllName(fieldsToReturn
);
487 for (String field
: fieldsToSnippet
) {
488 FieldExpression
.Builder expressionBuilder
= FieldExpression
.newBuilder().setName(field
);
489 expressionBuilder
.setExpression("snippet(" + quoteString(query
) + ", " + field
+ ")");
490 fieldSpec
.addExpression(expressionBuilder
.build().copyToProtocolBuffer());
492 for (FieldExpression expression
: expressionsToReturn
) {
493 fieldSpec
.addExpression(expression
.copyToProtocolBuffer());
495 builder
.setFieldSpec(fieldSpec
);
501 public String
toString() {
502 Util
.ToStringHelper helper
= new Util
.ToStringHelper("QueryOptions")
503 .addField("limit", limit
)
504 .addField("IDsOnly", idsOnly
)
505 .addField("sortOptions", sortOptions
)
506 .addIterableField("fieldsToReturn", fieldsToReturn
)
507 .addIterableField("fieldsToSnippet", fieldsToSnippet
)
508 .addIterableField("expressionsToReturn", expressionsToReturn
);
509 if (hasNumberFoundAccuracy()) {
510 helper
.addField("numberFoundAccuracy", numberFoundAccuracy
);
513 .addField("cursor", cursor
)
514 .addField("offset", offset
)