App Engine Python SDK version 1.9.8
[gae.git] / java / src / main / com / google / appengine / api / search / QueryOptions.java
blobd6636a5f7c93afcfaf32e2f32ac198e13866edab
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;
15 /**
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.
19 * <p>
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.
27 * <p>
28 * <pre>
29 * QueryOptions request = QueryOptions.newBuilder()
30 * .setLimit(20)
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())
40 * .build();
41 * </pre>
43 public final class QueryOptions {
45 /**
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;
56 private Builder() {
59 /**
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());
74 /**
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);
84 return this;
87 /**
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:
93 * <p>
94 * <pre>
95 * Cursor cursor = Cursor.newBuilder().build();
97 * SearchResults results = index.search(
98 * Query.newBuilder()
99 * .setOptions(QueryOptions.newBuilder()
100 * .setLimit(20)
101 * .setCursor(cursor)
102 * .build())
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();
114 * </pre>
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;
126 return this;
130 * Sets a cursor built from the builder.
132 * @see #setCursor(Cursor)
133 * @param cursorBuilder a {@link Cursor.Builder} that is used to build
134 * a {@link Cursor}.
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
145 * amount of results
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);
154 return this;
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
168 * acceptable range
170 public Builder setNumberFoundAccuracy(int numberFoundAccuracy) {
171 this.numberFoundAccuracy =
172 QueryOptionsChecker.checkNumberFoundAccuracy(numberFoundAccuracy);
173 return this;
177 * Clears any accuracy requirement for {@link Results#getNumberFound()}.
179 public Builder clearNumberFoundAccuracy() {
180 this.numberFoundAccuracy = SearchApiLimits.SEARCH_DEFAULT_NUMBER_FOUND_ACCURACY;
181 return this;
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)));
197 return this;
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)));
213 return this;
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;
247 return this;
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);
258 return this;
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;
269 return this;
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();
280 return this;
284 * Construct the final message.
286 * @return the QueryOptions built from the parameters entered on this
287 * Builder
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;
322 checkValid();
326 * @return the limit on the number of documents to return in search
327 * results
329 public int getLimit() {
330 return limit;
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() {
339 return cursor;
343 * @return the offset of the first result to return; returns 0 if
344 * was not set
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
371 * {@link Results}
373 public SortOptions getSortOptions() {
374 return sortOptions;
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
386 * results
388 public List<String> getFieldsToReturn() {
389 return fieldsToReturn;
393 * @return an unmodifiable list of names of fields to snippet in search
394 * results
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
438 * invalid
440 private QueryOptions checkValid() {
441 Preconditions.checkNotNull(limit, "number of documents to return cannot be null");
442 QueryOptionsChecker.checkFieldNames(fieldsToReturn);
443 return this;
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
463 * unknown
465 SearchParams.Builder copyToProtocolBuffer(SearchParams.Builder builder, String query) {
466 builder.setLimit(getLimit());
467 if (cursor != null) {
468 cursor.copyToProtocolBuffer(builder);
469 } else {
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);
497 return builder;
500 @Override
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);
512 return helper
513 .addField("cursor", cursor)
514 .addField("offset", offset)
515 .finish();