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())
42 public final class QueryOptions
{
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
;
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());
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
);
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:
93 * Cursor cursor = Cursor.newBuilder().build();
95 * SearchResults results = index.search(
97 * .setOptions(QueryOptions.newBuilder()
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();
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
;
128 * Sets a cursor built from the builder.
130 * @see #setCursor(Cursor)
131 * @param cursorBuilder a {@link Cursor.Builder} that is used to build
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
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
);
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
168 public Builder
setNumberFoundAccuracy(int numberFoundAccuracy
) {
169 this.numberFoundAccuracy
=
170 QueryOptionsChecker
.checkNumberFoundAccuracy(numberFoundAccuracy
);
175 * Clears any accuracy requirement for {@link Results#getNumberFound()}.
177 public Builder
clearNumberFoundAccuracy() {
178 this.numberFoundAccuracy
= SearchApiLimits
.SEARCH_DEFAULT_NUMBER_FOUND_ACCURACY
;
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
);
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
);
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
;
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
);
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
;
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();
288 * Construct the final message.
290 * @return the QueryOptions built from the parameters entered on this
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
;
330 * @return the limit on the number of documents to return in search
333 public int getLimit() {
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() {
347 * @return the offset of the first result to return; returns 0 if
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
377 public SortOptions
getSortOptions() {
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
392 public List
<String
> getFieldsToReturn() {
393 return Collections
.unmodifiableList(fieldsToReturn
);
397 * @return an unmodifiable list of names of fields to snippet in search
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
444 private QueryOptions
checkValid() {
445 Preconditions
.checkNotNull(limit
, "number of documents to return cannot be null");
446 QueryOptionsChecker
.checkFieldNames(fieldsToReturn
);
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
469 SearchParams
.Builder
copyToProtocolBuffer(SearchParams
.Builder builder
, String query
) {
470 builder
.setLimit(getLimit());
471 if (cursor
!= null) {
472 cursor
.copyToProtocolBuffer(builder
);
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
);
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
);
517 .addField("cursor", cursor
)
518 .addField("offset", offset
)