Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / search / QueryOptions.java
blob1f891fd625752dde43d168a3b4e1193f5baa6215
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;
10 import java.util.ArrayList;
11 import java.util.Collections;
12 import java.util.List;
14 /**
15 * Represents options which control where and what in the search results to
16 * return, from restricting the document fields returned to those given, and
17 * scoring and sorting the results, whilst supporting pagination.
18 * <p>
19 * For example, the following options will return documents from search
20 * results for some given query, returning up to 20 results including the
21 * fields 'author' and 'date-sent' as well as snippeted fields 'subject' and
22 * 'body'. The results are sorted by 'author' in descending order, getting
23 * the next 20 results from the responseCursor in the previously returned
24 * results, giving back a single cursor in the {@link Results} to
25 * get the next batch of results after this.
26 * <p>
27 * <pre>
28 * QueryOptions request = QueryOptions.newBuilder()
29 * .setLimit(20)
30 * .setFieldsToReturn("author", "date-sent")
31 * .setFieldsToSnippet("subject", "body")
32 * .setSortOptions(SortOptions.newBuilder().
33 * .addSortExpression(SortExpression.newBuilder()
34 * .setExpression("author")
35 * .setDirection(Scorer.SortDirection.DESCENDING)
36 * .setDefaultValue("")))
38 * .setCursor(Cursor.newBuilder().build())
39 * .build();
40 * </pre>
42 public final class QueryOptions {
44 /**
45 * A builder which constructs QueryOptions objects.
47 public static final class Builder {
48 private Integer limit;
50 private List<String> fieldsToReturn = new ArrayList<String>();
51 private List<String> fieldsToSnippet = new ArrayList<String>();
52 private List<FieldExpression> expressionsToReturn = new ArrayList<FieldExpression>();
53 private SortOptions sortOptions; private Cursor cursor; private Integer numberFoundAccuracy; private Integer offset; private Boolean idsOnly;
55 private Builder() {
58 /**
59 * Constructs a {@link QueryOptions} builder with the given request.
61 * @param request the search request to populate the builder
63 private Builder(QueryOptions request) {
64 limit = request.getLimit();
65 cursor = request.getCursor();
66 numberFoundAccuracy = request.getNumberFoundAccuracy();
67 sortOptions = request.getSortOptions();
68 fieldsToReturn = new ArrayList<String>(request.getFieldsToReturn());
69 expressionsToReturn = new ArrayList<FieldExpression>(request.getExpressionsToReturn());
72 /**
73 * Sets the limit on the number of documents to return in {@link Results}.
75 * @param limit the number of documents to return
76 * @return this Builder
77 * @throws IllegalArgumentException if numDocumentsToReturn is
78 * not within acceptable range
80 public Builder setLimit(int limit) {
81 this.limit = QueryOptionsChecker.checkLimit(limit);
82 return this;
85 /**
86 * Sets the cursor. The cursor is obtained from either a
87 * {@link Results} or one of the individual
88 * {@link ScoredDocument ScoredDocuments}.
90 * This is illustrated from the following code fragment:
91 * <p>
92 * <pre>
93 * Cursor cursor = Cursor.newBuilder().build();
95 * SearchResults results = index.search(
96 * Query.newBuilder()
97 * .setOptions(QueryOptions.newBuilder()
98 * .setLimit(20)
99 * .setCursor(cursor)
100 * .build())
101 * .build("some query"));
103 * // If the Cursor is built without setPerResult(true), then
104 * // by default a single {@link Cursor} is returned with the
105 * // {@link Results}.
106 * cursor = results.getCursor();
108 * for (ScoredDocument result : results) {
109 * // If you set Cursor.newBuilder().setPerResult(true)
110 * // then a cursor is returned with each result.
111 * result.getCursor();
112 * </pre>
114 * @param cursor use a cursor returned from a
115 * previous set of search results as a starting point to retrieve
116 * the next set of results. This can get you better performance, and
117 * also improves the consistency of pagination through index updates
118 * @return this Builder
120 public Builder setCursor(Cursor cursor) {
121 Preconditions.checkArgument(offset == null || cursor == null,
122 "offset and cursor cannot be set in the same request");
123 this.cursor = cursor;
124 return this;
128 * Sets a cursor built from the builder.
130 * @see #setCursor(Cursor)
131 * @param cursorBuilder a {@link Cursor.Builder} that is used to build
132 * a {@link Cursor}.
133 * @return this Builder
135 public Builder setCursor(Cursor.Builder cursorBuilder) {
136 return setCursor(cursorBuilder.build());
140 * Sets the offset of the first result to return.
142 * @param offset the offset into all search results to return the limit
143 * amount of results
144 * @return this Builder
145 * @throws IllegalArgumentException if the offset is negative or is larger
146 * than {@link SearchApiLimits#SEARCH_MAXIMUM_OFFSET}
148 public Builder setOffset(int offset) {
149 Preconditions.checkArgument(cursor == null,
150 "offset and cursor cannot be set in the same request");
151 this.offset = QueryOptionsChecker.checkOffset(offset);
152 return this;
156 * Sets the accuracy requirement for
157 * {@link Results#getNumberFound()}. If set,
158 * {@code getNumberFound()} will be accurate up to at least that number.
159 * For example, when set to 100, any {@code getNumberFound()} <= 100 is
160 * accurate. This option may add considerable latency / expense, especially
161 * when used with {@link Builder#setFieldsToReturn(String...)}.
163 * @param numberFoundAccuracy the minimum accuracy requirement
164 * @return this Builder
165 * @throws IllegalArgumentException if the accuracy is not within
166 * acceptable range
168 public Builder setNumberFoundAccuracy(int numberFoundAccuracy) {
169 this.numberFoundAccuracy =
170 QueryOptionsChecker.checkNumberFoundAccuracy(numberFoundAccuracy);
171 return this;
175 * Clears any accuracy requirement for {@link Results#getNumberFound()}.
177 public Builder clearNumberFoundAccuracy() {
178 this.numberFoundAccuracy = SearchApiLimits.SEARCH_DEFAULT_NUMBER_FOUND_ACCURACY;
179 return this;
183 * Specifies one or more fields to return in results.
185 * @param fields the names of fields to return in results
186 * @return this Builder
187 * @throws IllegalArgumentException if any of the field names is invalid
189 public Builder setFieldsToReturn(String... fields) {
190 Preconditions.checkNotNull(fields, "field names cannot be null");
191 Preconditions.checkArgument(idsOnly == null,
192 "You may not set fields to return if search returns keys only");
193 List<String> returningFields = new ArrayList<String>(fields.length);
194 for (String field : fields) {
195 returningFields.add(field);
197 fieldsToReturn = QueryOptionsChecker.checkFieldNames(returningFields);
198 return this;
202 * Specifies one or more fields to snippet in results. Snippets will be
203 * returned as fields with the same names in
204 * {@link ScoredDocument#getExpressions()}.
206 * @param fieldsToSnippet the names of fields to snippet in results
207 * @return this Builder
208 * @throws IllegalArgumentException if any of the field names is invalid
210 public Builder setFieldsToSnippet(String... fieldsToSnippet) {
211 Preconditions.checkNotNull(fieldsToReturn, "field names cannot be null");
212 List<String> snippetingFields = new ArrayList<String>(fieldsToSnippet.length);
213 for (String field : fieldsToSnippet) {
214 snippetingFields.add(field);
216 this.fieldsToSnippet = QueryOptionsChecker.checkFieldNames(snippetingFields);
217 return this;
221 * Adds a {@link FieldExpression} build from the given
222 * {@code expressionBuilder} to return in search results. Snippets will be
223 * returned as fields with the same names in
224 * {@link ScoredDocument#getExpressions()}.
226 * @param expressionBuilder a builder of named expressions to
227 * evaluate and return in results
228 * @return this Builder
230 public Builder addExpressionToReturn(FieldExpression.Builder expressionBuilder) {
231 Preconditions.checkArgument(idsOnly == null,
232 "You may not add expressions to return if search returns keys only");
233 return addExpressionToReturn(expressionBuilder.build());
237 * Sets whether or not the search should return documents or document IDs only.
238 * This setting is incompatible with
239 * {@link #addExpressionToReturn(FieldExpression)} and with
240 * {@link #setFieldsToReturn(String...)} methods.
242 * @param idsOnly whether or not only IDs of documents are returned by search request
243 * @return this Builder
245 public Builder setReturningIdsOnly(boolean idsOnly) {
246 Preconditions.checkArgument(expressionsToReturn.isEmpty(),
247 "You cannot request IDs only if expressions to return are set");
248 Preconditions.checkArgument(fieldsToReturn.isEmpty(),
249 "You cannot request IDs only if fields to return are already set");
250 this.idsOnly = idsOnly;
251 return this;
255 * Adds a {@link FieldExpression} to return in search results.
257 * @param expression a named expression to compute and return in results
258 * @return this Builder
260 public Builder addExpressionToReturn(FieldExpression expression) {
261 this.expressionsToReturn.add(expression);
262 return this;
266 * Sets a {@link SortOptions} to sort documents with.
268 * @param sortOptions specifies how to sort the documents in {@link Results}
269 * @return this Builder
271 public Builder setSortOptions(SortOptions sortOptions) {
272 this.sortOptions = sortOptions;
273 return this;
277 * Sets a {@link SortOptions} using a builder.
279 * @param builder a builder of a {@link SortOptions}
280 * @return this Builder
282 public Builder setSortOptions(SortOptions.Builder builder) {
283 this.sortOptions = builder.build();
284 return this;
288 * Construct the final message.
290 * @return the QueryOptions built from the parameters entered on this
291 * Builder
292 * @throws IllegalArgumentException if the search request is invalid
294 public QueryOptions build() {
295 return new QueryOptions(this);
299 private final int limit;
301 private final int numberFoundAccuracy;
303 private final List<String> fieldsToReturn;
304 private final List<String> fieldsToSnippet;
305 private final List<FieldExpression> expressionsToReturn;
306 private final SortOptions sortOptions; private final Cursor cursor; private final Integer offset; private final Boolean idsOnly;
309 * Creates a search request from the builder.
311 * @param builder the search request builder to populate with
313 private QueryOptions(Builder builder) {
314 limit = QueryOptionsChecker.checkLimit(
315 Util.defaultIfNull(builder.limit, SearchApiLimits.SEARCH_DEFAULT_LIMIT));
316 numberFoundAccuracy = Util.defaultIfNull(builder.numberFoundAccuracy,
317 SearchApiLimits.SEARCH_DEFAULT_NUMBER_FOUND_ACCURACY);
318 sortOptions = builder.sortOptions;
319 cursor = builder.cursor;
320 offset = QueryOptionsChecker.checkOffset(builder.offset);
322 fieldsToReturn = new ArrayList<String>(builder.fieldsToReturn);
323 fieldsToSnippet = new ArrayList<String>(builder.fieldsToSnippet);
324 expressionsToReturn = new ArrayList<FieldExpression>(builder.expressionsToReturn);
325 idsOnly = builder.idsOnly;
326 checkValid();
330 * @return the limit on the number of documents to return in search
331 * results
333 public int getLimit() {
334 return limit;
338 * @return a cursor returned from a previous set of
339 * search results to use as a starting point to retrieve the next
340 * set of results. Can be null
342 public Cursor getCursor() {
343 return cursor;
347 * @return the offset of the first result to return; returns 0 if
348 * was not set
350 public int getOffset() {
351 return (offset == null) ? 0 : offset.intValue();
355 * Returns true iff there is an accuracy requirement set.
357 * @return the found count accuracy
359 public boolean hasNumberFoundAccuracy() {
360 return numberFoundAccuracy != SearchApiLimits.SEARCH_DEFAULT_NUMBER_FOUND_ACCURACY;
364 * Any {@link Results#getNumberFound()} less than or equal to this
365 * setting will be accurate.
367 * @return the found count accuracy
369 public int getNumberFoundAccuracy() {
370 return numberFoundAccuracy;
374 * @return a {@link SortOptions} specifying how to sort Documents in
375 * {@link Results}
377 public SortOptions getSortOptions() {
378 return sortOptions;
382 * @return if this search request returns results document IDs only
384 public boolean isReturningIdsOnly() {
385 return idsOnly == null ? false : idsOnly.booleanValue();
389 * @return an unmodifiable list of names of fields to return in search
390 * results
392 public List<String> getFieldsToReturn() {
393 return Collections.unmodifiableList(fieldsToReturn);
397 * @return an unmodifiable list of names of fields to snippet in search
398 * results
400 public List<String> getFieldsToSnippet() {
401 return Collections.unmodifiableList(fieldsToSnippet);
405 * @return an unmodifiable list of expressions which will be evaluated
406 * and returned in results
408 public List<FieldExpression> getExpressionsToReturn() {
409 return Collections.unmodifiableList(expressionsToReturn);
413 * Creates and returns a {@link QueryOptions} builder. Set the search request
414 * parameters and use the {@link Builder#build()} method to create a concrete
415 * instance of QueryOptions.
417 * @return a {@link Builder} which can construct a search request
419 public static Builder newBuilder() {
420 return new Builder();
424 * Creates a builder from the given request.
426 * @param request the search request for the builder to use
427 * to build another request
428 * @return a new builder with values set from the given request
430 public static Builder newBuilder(QueryOptions request) {
431 return new Builder(request);
435 * Checks the search specification is valid, specifically, has
436 * a non-null number of documents to return specification, a valid
437 * cursor if present, valid sort specification list, a valid
438 * collection of field names for sorting.
440 * @return this checked QueryOptions
441 * @throws IllegalArgumentException if some part of the specification is
442 * invalid
444 private QueryOptions checkValid() {
445 Preconditions.checkNotNull(limit, "number of documents to return cannot be null");
446 QueryOptionsChecker.checkFieldNames(fieldsToReturn);
447 return this;
451 * Wraps quotes around an escaped argument string.
453 * @param argument the string to escape quotes and wrap with quotes
454 * @return the wrapped and escaped argument string
456 private static String quoteString(String argument) {
457 return "\"" + argument.replace("\"", "\\\"") + "\"";
461 * Copies the contents of this {@link QueryOptions} object into a
462 * {@link SearchParams} protocol buffer builder.
464 * @return a search params protocol buffer builder initialized with
465 * the values from this request
466 * @throws IllegalArgumentException if the cursor type is
467 * unknown
469 SearchParams.Builder copyToProtocolBuffer(SearchParams.Builder builder, String query) {
470 builder.setLimit(getLimit());
471 if (cursor != null) {
472 cursor.copyToProtocolBuffer(builder);
473 } else {
474 builder.setCursorType(SearchParams.CursorType.NONE);
476 if (offset != null) {
477 builder.setOffset(offset);
479 if (idsOnly != null) {
480 builder.setKeysOnly(idsOnly);
482 if (hasNumberFoundAccuracy()) {
483 builder.setMatchedCountAccuracy(numberFoundAccuracy);
485 if (sortOptions != null) {
486 sortOptions.copyToProtocolBuffer(builder);
488 if (!fieldsToReturn.isEmpty() || !fieldsToSnippet.isEmpty() || !expressionsToReturn.isEmpty()) {
489 SearchServicePb.FieldSpec.Builder fieldSpec = SearchServicePb.FieldSpec.newBuilder();
490 fieldSpec.addAllName(fieldsToReturn);
491 for (String field : fieldsToSnippet) {
492 FieldExpression.Builder expressionBuilder = FieldExpression.newBuilder().setName(field);
493 expressionBuilder.setExpression("snippet(" + quoteString(query) + ", " + field + ")");
494 fieldSpec.addExpression(expressionBuilder.build().copyToProtocolBuffer());
496 for (FieldExpression expression : expressionsToReturn) {
497 fieldSpec.addExpression(expression.copyToProtocolBuffer());
499 builder.setFieldSpec(fieldSpec);
501 return builder;
504 @Override
505 public String toString() {
506 Util.ToStringHelper helper = new Util.ToStringHelper("QueryOptions")
507 .addField("limit", limit)
508 .addField("IDsOnly", idsOnly)
509 .addField("sortOptions", sortOptions)
510 .addIterableField("fieldsToReturn", fieldsToReturn)
511 .addIterableField("fieldsToSnippet", fieldsToSnippet)
512 .addIterableField("expressionsToReturn", expressionsToReturn);
513 if (hasNumberFoundAccuracy()) {
514 helper.addField("numberFoundAccuracy", numberFoundAccuracy);
516 return helper
517 .addField("cursor", cursor)
518 .addField("offset", offset)
519 .finish();