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
;
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.
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.
28 * QueryOptions request = QueryOptions.newBuilder()
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())
43 public final class QueryOptions
{
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
;
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());
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
);
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:
94 * Cursor cursor = Cursor.newBuilder().build();
96 * SearchResults results = index.search(
98 * .setOptions(QueryOptions.newBuilder()
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();
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
;
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
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
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
);
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
169 public Builder
setNumberFoundAccuracy(int numberFoundAccuracy
) {
170 this.numberFoundAccuracy
=
171 QueryOptionsChecker
.checkNumberFoundAccuracy(numberFoundAccuracy
);
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
);
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
);
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
;
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
);
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
;
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();
281 * Construct the final message.
283 * @return the QueryOptions built from the parameters entered on this
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
;
323 * @return the limit on the number of documents to return in search
326 public int getLimit() {
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() {
340 * @return the offset of the first result to return; returns 0 if
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
361 public SortOptions
getSortOptions() {
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
376 public List
<String
> getFieldsToReturn() {
377 return Collections
.unmodifiableList(fieldsToReturn
);
381 * @return an unmodifiable list of names of fields to snippet in search
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
428 private QueryOptions
checkValid() {
429 Preconditions
.checkNotNull(limit
, "number of documents to return cannot be null");
430 QueryOptionsChecker
.checkFieldNames(fieldsToReturn
);
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
453 SearchParams
.Builder
copyToProtocolBuffer(SearchParams
.Builder builder
, String query
) {
454 builder
.setLimit(getLimit());
455 if (cursor
!= null) {
456 cursor
.copyToProtocolBuffer(builder
);
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
);
488 public String
toString() {
489 return String
.format(
490 "QueryOptions(limit=%d%s%s%s%s%s, numberFoundAccuracy=%d%s%s)",
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
),
498 Util
.fieldToString("cursor", cursor
),
499 Util
.fieldToString("offset", offset
));