Version 1.7.4
[gae.git] / java / src / main / com / google / appengine / api / search / QueryOptions.java
blob3c36f802e52592af59f7b477b7b32128f44a368b
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>
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 List<String> fieldsToReturn = new ArrayList<String>();
52 private List<String> fieldsToSnippet = new ArrayList<String>();
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 = new ArrayList<String>(request.getFieldsToReturn());
70 expressionsToReturn = new ArrayList<FieldExpression>(request.getExpressionsToReturn());
73 /**
74 * Sets the limit on the number of documents to return in {@link Results}.
76 * @param limit the number of documents to return
77 * @return this Builder
78 * @throws IllegalArgumentException if numDocumentsToReturn is
79 * not within acceptable range
81 public Builder setLimit(int limit) {
82 this.limit = QueryOptionsChecker.checkLimit(limit);
83 return this;
86 /**
87 * Sets the cursor. The cursor is obtained from either a
88 * {@link Results} or one of the individual
89 * {@link ScoredDocument ScoredDocuments}.
91 * This is illustrated from the following code fragment:
92 * <p>
93 * <pre>
94 * Cursor cursor = Cursor.newBuilder().build();
96 * SearchResults results = index.search(
97 * Query.newBuilder()
98 * .setOptions(QueryOptions.newBuilder()
99 * .setLimit(20)
100 * .setCursor(cursor)
101 * .build())
102 * .build("some query"));
104 * // If the Cursor is built without setPerResult(true), then
105 * // by default a single {@link Cursor} is returned with the
106 * // {@link Results}.
107 * cursor = results.getCursor();
109 * for (ScoredDocument result : results) {
110 * // If you set Cursor.newBuilder().setPerResult(true)
111 * // then a cursor is returned with each result.
112 * result.getCursor();
113 * </pre>
115 * @param cursor use a cursor returned from a
116 * previous set of search results as a starting point to retrieve
117 * the next set of results. This can get you better performance, and
118 * also improves the consistency of pagination through index updates
119 * @return this Builder
121 public Builder setCursor(Cursor cursor) {
122 Preconditions.checkArgument(offset == null || cursor == null,
123 "offset and cursor cannot be set in the same request");
124 this.cursor = cursor;
125 return this;
129 * Sets a cursor built from the builder.
131 * @see {@link #setCursor(Cursor)}
132 * @param cursorBuilder a {@link Cursor.Builder} that is used to build
133 * a {@link} Cursor
134 * @return this Builder
136 public Builder setCursor(Cursor.Builder cursorBuilder) {
137 return setCursor(cursorBuilder.build());
141 * Sets the offset of the first result to return.
143 * @param offset the offset into all search results to return the limit
144 * amount of results
145 * @return this Builder
146 * @throws IllegalArgumentException if the offset is negative or is larger
147 * than {@link SearchApiLimits#SEARCH_MAXIMUM_OFFSET}
149 public Builder setOffset(int offset) {
150 Preconditions.checkArgument(cursor == null,
151 "offset and cursor cannot be set in the same request");
152 this.offset = QueryOptionsChecker.checkOffset(offset);
153 return this;
157 * Sets the accuracy requirement for
158 * {@link Results#getNumberFound()}. If set,
159 * {@code getNumberFound()} will be accurate up to at least that number.
160 * For example, when set to 100, any {@code getNumberFound()} <= 100 is
161 * accurate. This option may add considerable latency / expense, especially
162 * when used with {@link Builder#setFieldsToReturn(String...)}.
164 * @param numberFoundAccuracy the minimum accuracy requirement
165 * @return this Builder
166 * @throws IllegalArgumentException if the accuracy is not within
167 * acceptable range
169 public Builder setNumberFoundAccuracy(int numberFoundAccuracy) {
170 this.numberFoundAccuracy =
171 QueryOptionsChecker.checkNumberFoundAccuracy(numberFoundAccuracy);
172 return this;
176 * Specifies one or more fields to return in results.
178 * @param fields the names of fields to return in results
179 * @return this Builder
180 * @throws IllegalArgumentException if any of the field names is invalid
182 public Builder setFieldsToReturn(String... fields) {
183 Preconditions.checkNotNull(fields, "field names cannot be null");
184 Preconditions.checkArgument(idsOnly == null,
185 "You may not set fields to return if search returns keys only");
186 List<String> returningFields = new ArrayList<String>(fields.length);
187 for (String field : fields) {
188 returningFields.add(field);
190 fieldsToReturn = QueryOptionsChecker.checkFieldNames(returningFields);
191 return this;
195 * Specifies one or more fields to snippet in results. Snippets will be
196 * returned as fields with the same names in
197 * {@link ScoredDocument#getExpressions()}.
199 * @param fieldsToSnippet the names of fields to snippet in results
200 * @return this Builder
201 * @throws IllegalArgumentException if any of the field names is invalid
203 public Builder setFieldsToSnippet(String... fieldsToSnippet) {
204 Preconditions.checkNotNull(fieldsToReturn, "field names cannot be null");
205 List<String> snippetingFields = new ArrayList<String>(fieldsToSnippet.length);
206 for (String field : fieldsToSnippet) {
207 snippetingFields.add(field);
209 this.fieldsToSnippet = QueryOptionsChecker.checkFieldNames(snippetingFields);
210 return this;
214 * Adds a {@link FieldExpression} build from the given
215 * {@code expressionBuilder} to return in search results. Snippets will be
216 * returned as fields with the same names in
217 * {@link ScoredDocument#getExpressions()}.
219 * @param expressionBuilder a builder of named expressions to
220 * evaluate and return in results
221 * @return this Builder
223 public Builder addExpressionToReturn(FieldExpression.Builder expressionBuilder) {
224 Preconditions.checkArgument(idsOnly == null,
225 "You may not add expressions to return if search returns keys only");
226 return addExpressionToReturn(expressionBuilder.build());
230 * Sets whether or not the search should return documents or document IDs only.
231 * This setting is incompatible with
232 * {@link #addExpressionToReturn(FieldExpression)} and with
233 * {@link #setFieldsToReturn(String...)} methods.
235 * @param idsOnly whether or not only IDs of documents are returned by search request
236 * @return this Builder
238 public Builder setReturningIdsOnly(boolean idsOnly) {
239 Preconditions.checkArgument(expressionsToReturn.isEmpty(),
240 "You cannot request IDs only if expressions to return are set");
241 Preconditions.checkArgument(fieldsToReturn.isEmpty(),
242 "You cannot request IDs only if fields to return are already set");
243 this.idsOnly = idsOnly;
244 return this;
248 * Adds a {@link FieldExpression} to return in search results.
250 * @param expression a named expression to compute and return in results
251 * @return this Builder
253 public Builder addExpressionToReturn(FieldExpression expression) {
254 this.expressionsToReturn.add(expression);
255 return this;
259 * Sets a {@link SortOptions} to sort documents with.
261 * @param sortOptions specifies how to sort the documents in {@link Results}
262 * @return this Builder
264 public Builder setSortOptions(SortOptions sortOptions) {
265 this.sortOptions = sortOptions;
266 return this;
270 * Sets a {@link SortOptions} using a builder.
272 * @param builder a builder of a {@link SortOptions}
273 * @return this Builder
275 public Builder setSortOptions(SortOptions.Builder builder) {
276 this.sortOptions = builder.build();
277 return this;
281 * Construct the final message.
283 * @return the QueryOptions built from the parameters entered on this
284 * Builder
285 * @throws IllegalArgumentException if the search request is invalid
287 public QueryOptions build() {
288 return new QueryOptions(this);
292 private final int limit;
294 private final int numberFoundAccuracy;
296 private final List<String> fieldsToReturn;
297 private final List<String> fieldsToSnippet;
298 private final List<FieldExpression> expressionsToReturn;
299 private final SortOptions sortOptions; private final Cursor cursor; private final Integer offset; private final Boolean idsOnly;
302 * Creates a search request from the builder.
304 * @param builder the search request builder to populate with
306 private QueryOptions(Builder builder) {
307 limit = QueryOptionsChecker.checkLimit(
308 Util.defaultIfNull(builder.limit, SearchApiLimits.SEARCH_DEFAULT_LIMIT));
309 numberFoundAccuracy = Util.defaultIfNull(builder.numberFoundAccuracy,
310 SearchApiLimits.SEARCH_DEFAULT_NUMBER_FOUND_ACCURACY);
311 sortOptions = builder.sortOptions;
312 cursor = builder.cursor;
313 offset = QueryOptionsChecker.checkOffset(builder.offset);
315 fieldsToReturn = new ArrayList<String>(builder.fieldsToReturn);
316 fieldsToSnippet = new ArrayList<String>(builder.fieldsToSnippet);
317 expressionsToReturn = new ArrayList<FieldExpression>(builder.expressionsToReturn);
318 idsOnly = builder.idsOnly;
319 checkValid();
323 * @return the limit on the number of documents to return in search
324 * results
326 public int getLimit() {
327 return limit;
331 * @return a cursor returned from a previous set of
332 * search results to use as a starting point to retrieve the next
333 * set of results. Can be null
335 public Cursor getCursor() {
336 return cursor;
340 * @return the offset of the first result to return; returns 0 if
341 * was not set
343 public int getOffset() {
344 return (offset == null) ? 0 : offset.intValue();
348 * Any {@link Results#getNumberFound()} less than or equal to this
349 * setting will be accurate.
351 * @return the found count accuracy
353 public int getNumberFoundAccuracy() {
354 return numberFoundAccuracy;
358 * @return a {@link SortOptions} specifying how to sort Documents in
359 * {@link Results}
361 public SortOptions getSortOptions() {
362 return sortOptions;
366 * @return if this search request returns results document IDs only
368 public boolean isReturningIdsOnly() {
369 return idsOnly == null ? false : idsOnly.booleanValue();
373 * @return an unmodifiable list of names of fields to return in search
374 * results
376 public List<String> getFieldsToReturn() {
377 return Collections.unmodifiableList(fieldsToReturn);
381 * @return an unmodifiable list of names of fields to snippet in search
382 * results
384 public List<String> getFieldsToSnippet() {
385 return Collections.unmodifiableList(fieldsToSnippet);
389 * @return an unmodifiable list of expressions which will be evaluated
390 * and returned in results
392 public List<FieldExpression> getExpressionsToReturn() {
393 return Collections.unmodifiableList(expressionsToReturn);
397 * Creates and returns a {@link QueryOptions} builder. Set the search request
398 * parameters and use the {@link Builder#build()} method to create a concrete
399 * instance of QueryOptions.
401 * @return a {@link Builder} which can construct a search request
403 public static Builder newBuilder() {
404 return new Builder();
408 * Creates a builder from the given request.
410 * @param request the search request for the builder to use
411 * to build another request
412 * @return a new builder with values set from the given request
414 public static Builder newBuilder(QueryOptions request) {
415 return new Builder(request);
419 * Checks the search specification is valid, specifically, has
420 * a non-null number of documents to return specification, a valid
421 * cursor if present, valid sort specification list, a valid
422 * collection of field names for sorting.
424 * @return this checked QueryOptions
425 * @throws IllegalArgumentException if some part of the specification is
426 * invalid
428 private QueryOptions checkValid() {
429 Preconditions.checkNotNull(limit, "number of documents to return cannot be null");
430 QueryOptionsChecker.checkFieldNames(fieldsToReturn);
431 return this;
435 * Wraps quotes around an escaped argument string.
437 * @param argument the string to escape quotes and wrap with quotes
438 * @return the wrapped and escaped argument string
440 private static String quoteString(String argument) {
441 return "\"" + argument.replace("\"", "\\\"") + "\"";
445 * Copies the contents of this {@link QueryOptions} object into a
446 * {@link SearchParams} protocol buffer builder.
448 * @return a search params protocol buffer builder initialized with
449 * the values from this request
450 * @throws IllegalArgumentException if the cursor type is
451 * unknown
453 SearchParams.Builder copyToProtocolBuffer(SearchParams.Builder builder, String query) {
454 builder.setLimit(getLimit());
455 if (cursor != null) {
456 cursor.copyToProtocolBuffer(builder);
457 } else {
458 builder.setCursorType(SearchParams.CursorType.NONE);
460 if (offset != null) {
461 builder.setOffset(offset);
463 if (idsOnly != null) {
464 builder.setKeysOnly(idsOnly);
467 builder.setMatchedCountAccuracy(numberFoundAccuracy);
468 if (sortOptions != null) {
469 sortOptions.copyToProtocolBuffer(builder);
471 if (!fieldsToReturn.isEmpty() || !fieldsToSnippet.isEmpty() || !expressionsToReturn.isEmpty()) {
472 SearchServicePb.FieldSpec.Builder fieldSpec = SearchServicePb.FieldSpec.newBuilder();
473 fieldSpec.addAllName(fieldsToReturn);
474 for (String field : fieldsToSnippet) {
475 FieldExpression.Builder expressionBuilder = FieldExpression.newBuilder().setName(field);
476 expressionBuilder.setExpression("snippet(" + quoteString(query) + ", " + field + ")");
477 fieldSpec.addExpression(expressionBuilder.build().copyToProtocolBuffer());
479 for (FieldExpression expression : expressionsToReturn) {
480 fieldSpec.addExpression(expression.copyToProtocolBuffer());
482 builder.setFieldSpec(fieldSpec);
484 return builder;
487 @Override
488 public String toString() {
489 return String.format(
490 "QueryOptions(limit=%d%s%s%s%s%s, numberFoundAccuracy=%d%s%s)",
491 limit,
492 Util.fieldToString("IDsOnly", idsOnly),
493 Util.fieldToString("sortOptions", sortOptions),
494 Util.iterableFieldToString("fieldsToReturn", fieldsToReturn),
495 Util.iterableFieldToString("fieldsToSnippet", fieldsToSnippet),
496 Util.iterableFieldToString("expressionsToReturn", expressionsToReturn),
497 numberFoundAccuracy,
498 Util.fieldToString("cursor", cursor),
499 Util.fieldToString("offset", offset));