1 package com
.google
.appengine
.api
.search
;
3 import static com
.google
.appengine
.api
.search
.FutureHelper
.quietGet
;
5 import com
.google
.appengine
.api
.search
.SearchServicePb
.SearchServiceError
.ErrorCode
;
6 import com
.google
.appengine
.api
.search
.checkers
.DocumentChecker
;
7 import com
.google
.appengine
.api
.search
.checkers
.Preconditions
;
8 import com
.google
.appengine
.api
.search
.checkers
.SearchApiLimits
;
9 import com
.google
.appengine
.api
.utils
.FutureWrapper
;
10 import com
.google
.apphosting
.api
.search
.DocumentPb
;
11 import com
.google
.common
.collect
.Iterables
;
13 import java
.util
.ArrayList
;
14 import java
.util
.Arrays
;
15 import java
.util
.Collections
;
16 import java
.util
.HashMap
;
17 import java
.util
.List
;
19 import java
.util
.concurrent
.Future
;
22 * The default implementation of {@link Index}. This class uses a
23 * {@link SearchApiHelper} to forward all requests to an appserver.
25 class IndexImpl
implements Index
{
27 private final SearchApiHelper apiHelper
;
28 private final IndexSpec spec
;
29 private final Schema schema
;
30 private final SearchServiceConfig config
;
31 private final Long storageUsage
;
32 private final Long storageLimit
;
35 * Creates new index specification.
37 * @param apiHelper the helper used to forward all calls
38 * @param config a {@link SearchServiceConfig} instance that describes the
39 * index implementation.
40 * @param indexSpec the index specification
42 IndexImpl(SearchApiHelper apiHelper
, SearchServiceConfig config
, IndexSpec indexSpec
) {
43 this(apiHelper
, config
, indexSpec
, null, null, null);
47 * Creates new index specification.
49 * @param apiHelper the helper used to forward all calls
50 * @param config a {@link SearchServiceConfig} instance that describes the
51 * index implementation.
52 * @param indexSpec the index specification
53 * @param schema the {@link Schema} defining the names and types of fields
56 IndexImpl(SearchApiHelper apiHelper
, SearchServiceConfig config
, IndexSpec indexSpec
,
57 Schema schema
, Long amount
, Long limit
) {
58 this.apiHelper
= Preconditions
.checkNotNull(apiHelper
, "Internal error");
59 Preconditions
.checkNotNull(config
.getNamespace(), "Internal error");
60 this.spec
= Preconditions
.checkNotNull(indexSpec
, "Internal error");
63 this.storageUsage
= amount
;
64 this.storageLimit
= limit
;
68 public String
getName() {
69 return spec
.getName();
73 public String
getNamespace() {
74 return config
.getNamespace();
78 public Schema
getSchema() {
82 private RuntimeException
noStorageInfo() {
83 return new UnsupportedOperationException("Storage information is not available");
87 public long getStorageUsage() {
88 if (storageUsage
== null) {
89 throw noStorageInfo();
95 public long getStorageLimit() {
96 if (storageLimit
== null) {
97 throw noStorageInfo();
103 public int hashCode() {
104 final int prime
= 31;
106 result
= prime
* result
+ spec
.hashCode();
107 result
= prime
* result
+ ((schema
== null) ?
0 : schema
.hashCode());
108 result
= prime
* result
+ ((storageUsage
== null) ?
0 : storageUsage
.hashCode());
109 result
= prime
* result
+ ((storageLimit
== null) ?
0 : storageLimit
.hashCode());
114 public boolean equals(Object obj
) {
121 if (getClass() != obj
.getClass()) {
124 IndexImpl other
= (IndexImpl
) obj
;
125 return Util
.equalObjects(spec
, other
.spec
)
126 && Util
.equalObjects(schema
, other
.schema
)
127 && Util
.equalObjects(storageUsage
, other
.storageUsage
)
128 && Util
.equalObjects(storageLimit
, other
.storageLimit
);
132 public String
toString() {
134 (storageUsage
== null || storageLimit
== null)
135 ?
"(no storage data)"
136 : String
.format(" (%d/%d)", storageUsage
.longValue(), storageLimit
.longValue());
137 return String
.format("IndexImpl{namespace: %s, %s, %s%s}", config
.getNamespace(), spec
,
138 (schema
== null ?
"(null schema)" : schema
), storageInfo
);
143 public Future
<Void
> deleteSchemaAsync() {
144 SearchServicePb
.DeleteSchemaParams
.Builder builder
=
145 SearchServicePb
.DeleteSchemaParams
.newBuilder().addIndexSpec(
146 spec
.copyToProtocolBuffer(config
.getNamespace()));
148 Future
<SearchServicePb
.DeleteSchemaResponse
.Builder
> future
=
149 apiHelper
.makeAsyncDeleteSchemaCall(builder
, config
.getDeadline());
150 return new FutureWrapper
<SearchServicePb
.DeleteSchemaResponse
.Builder
,
153 protected Throwable
convertException(Throwable cause
) {
154 OperationResult result
= OperationResult
.convertToOperationResult(cause
);
155 return (result
== null) ? cause
: new DeleteException(result
);
159 protected Void
wrap(SearchServicePb
.DeleteSchemaResponse
.Builder key
)
161 SearchServicePb
.DeleteSchemaResponse response
= key
.build();
162 ArrayList
<OperationResult
> results
= new ArrayList
<>(
163 response
.getStatusCount());
164 for (SearchServicePb
.RequestStatus status
: response
.getStatusList()) {
165 results
.add(new OperationResult(status
));
167 if (response
.getStatusList().size() != 1) {
168 throw new DeleteException(
170 StatusCode
.INTERNAL_ERROR
,
171 String
.format("Expected 1 removed schema, but got %d",
172 response
.getStatusList().size())),
175 for (OperationResult result
: results
) {
176 if (result
.getCode() != StatusCode
.OK
) {
177 throw new DeleteException(result
, results
);
186 public Future
<Void
> deleteAsync(String
... documentIds
) {
187 return deleteAsync(Arrays
.asList(documentIds
));
191 public Future
<Void
> deleteAsync(final Iterable
<String
> documentIds
) {
192 Preconditions
.checkArgument(documentIds
!= null,
193 "Delete documents given null collection of document ids");
194 SearchServicePb
.DeleteDocumentParams
.Builder builder
=
195 SearchServicePb
.DeleteDocumentParams
.newBuilder().setIndexSpec(
196 spec
.copyToProtocolBuffer(config
.getNamespace()));
198 for (String documentId
: documentIds
) {
200 builder
.addDocId(DocumentChecker
.checkDocumentId(documentId
));
202 if (size
> SearchApiLimits
.PUT_MAXIMUM_DOCS_PER_REQUEST
) {
203 throw new IllegalArgumentException(
204 String
.format("number of doc ids, %s, exceeds maximum %s", size
,
205 SearchApiLimits
.PUT_MAXIMUM_DOCS_PER_REQUEST
));
207 final int documentIdsSize
= size
;
208 Future
<SearchServicePb
.DeleteDocumentResponse
.Builder
> future
=
209 apiHelper
.makeAsyncDeleteDocumentCall(builder
, config
.getDeadline());
210 return new FutureWrapper
<SearchServicePb
.DeleteDocumentResponse
.Builder
,
213 protected Throwable
convertException(Throwable cause
) {
214 OperationResult result
= OperationResult
.convertToOperationResult(cause
);
215 return (result
== null) ? cause
: new DeleteException(result
);
219 protected Void
wrap(SearchServicePb
.DeleteDocumentResponse
.Builder key
)
221 SearchServicePb
.DeleteDocumentResponse response
= key
.build();
222 ArrayList
<OperationResult
> results
= new ArrayList
<>(
223 response
.getStatusCount());
224 for (SearchServicePb
.RequestStatus status
: response
.getStatusList()) {
225 results
.add(new OperationResult(status
));
227 if (documentIdsSize
!= response
.getStatusList().size()) {
228 throw new DeleteException(
230 StatusCode
.INTERNAL_ERROR
,
231 String
.format("Expected %d removed documents, but got %d", documentIdsSize
,
232 response
.getStatusList().size())),
235 for (OperationResult result
: results
) {
236 if (result
.getCode() != StatusCode
.OK
) {
237 throw new DeleteException(result
, results
);
246 public Future
<PutResponse
> putAsync(Document
... documents
) {
247 return putAsync(Arrays
.asList(documents
));
251 public Future
<PutResponse
> putAsync(Document
.Builder
... builders
) {
252 List
<Document
> documents
= new ArrayList
<>();
253 for (int i
= 0; i
< builders
.length
; i
++) {
254 documents
.add(builders
[i
].build());
256 return putAsync(documents
);
260 public Future
<PutResponse
> putAsync(final Iterable
<Document
> documents
) {
261 Preconditions
.checkNotNull(documents
, "document list cannot be null");
262 if (Iterables
.isEmpty(documents
)) {
263 return new FutureHelper
.FakeFuture
<>(
264 new PutResponse(Collections
.<OperationResult
>emptyList(),
265 Collections
.<String
>emptyList()));
267 SearchServicePb
.IndexDocumentParams
.Builder builder
=
268 SearchServicePb
.IndexDocumentParams
.newBuilder()
269 .setIndexSpec(spec
.copyToProtocolBuffer(config
.getNamespace()));
270 Map
<String
, Document
> docMap
= new HashMap
<>();
272 for (Document document
: documents
) {
273 Document other
= null;
274 if (document
.getId() != null) {
275 other
= docMap
.put(document
.getId(), document
);
278 if (!document
.isIdenticalTo(other
)) {
279 throw new IllegalArgumentException(
281 "Put request with documents with the same ID \"%s\" but different content",
287 builder
.addDocument(Preconditions
.checkNotNull(document
, "document cannot be null")
288 .copyToProtocolBuffer());
291 if (size
> SearchApiLimits
.PUT_MAXIMUM_DOCS_PER_REQUEST
) {
292 throw new IllegalArgumentException(
293 String
.format("number of documents, %s, exceeds maximum %s", size
,
294 SearchApiLimits
.PUT_MAXIMUM_DOCS_PER_REQUEST
));
296 final int documentsSize
= size
;
297 Future
<SearchServicePb
.IndexDocumentResponse
.Builder
> future
=
298 apiHelper
.makeAsyncIndexDocumentCall(builder
, config
.getDeadline());
299 return new FutureWrapper
<SearchServicePb
.IndexDocumentResponse
.Builder
,
300 PutResponse
>(future
) {
302 protected Throwable
convertException(Throwable cause
) {
303 OperationResult result
= OperationResult
.convertToOperationResult(cause
);
304 return (result
== null) ? cause
: new PutException(result
);
308 protected PutResponse
wrap(SearchServicePb
.IndexDocumentResponse
.Builder key
)
310 SearchServicePb
.IndexDocumentResponse response
= key
.build();
311 List
<OperationResult
> results
= newOperationResultList(response
);
312 if (documentsSize
!= response
.getStatusList().size()) {
313 throw new PutException(
315 StatusCode
.INTERNAL_ERROR
,
316 String
.format("Expected %d indexed documents, but got %d", documentsSize
,
317 response
.getStatusList().size())), results
, response
.getDocIdList());
319 for (OperationResult result
: results
) {
320 if (result
.getCode() != StatusCode
.OK
) {
321 throw new PutException(result
, results
, response
.getDocIdList());
324 return new PutResponse(results
, response
.getDocIdList());
328 * Constructs a list of OperationResult from an index document response.
330 * @param response the index document response to extract operation
332 * @return a list of OperationResult
334 private List
<OperationResult
> newOperationResultList(
335 SearchServicePb
.IndexDocumentResponse response
) {
336 ArrayList
<OperationResult
> results
= new ArrayList
<>(
337 response
.getStatusCount());
338 for (SearchServicePb
.RequestStatus status
: response
.getStatusList()) {
339 results
.add(new OperationResult(status
));
346 private Future
<Results
<ScoredDocument
>> executeSearchForResults(
347 SearchServicePb
.SearchParams
.Builder params
) {
348 Future
<SearchServicePb
.SearchResponse
.Builder
> future
=
349 apiHelper
.makeAsyncSearchCall(params
, config
.getDeadline());
350 return new FutureWrapper
<SearchServicePb
.SearchResponse
.Builder
,
351 Results
<ScoredDocument
>>(future
) {
353 protected Throwable
convertException(Throwable cause
) {
354 OperationResult result
= OperationResult
.convertToOperationResult(cause
);
355 return (result
== null) ? cause
: new SearchException(result
);
359 protected Results
<ScoredDocument
> wrap(SearchServicePb
.SearchResponse
.Builder key
)
361 SearchServicePb
.SearchResponse response
= key
.build();
362 SearchServicePb
.RequestStatus status
= response
.getStatus();
363 if (status
.getCode() != SearchServicePb
.SearchServiceError
.ErrorCode
.OK
) {
364 throw new SearchException(new OperationResult(status
));
366 List
<ScoredDocument
> scoredDocs
= new ArrayList
<>();
367 for (SearchServicePb
.SearchResult result
: response
.getResultList()) {
368 List
<Field
> expressions
= new ArrayList
<>();
369 for (DocumentPb
.Field expression
: result
.getExpressionList()) {
370 expressions
.add(Field
.newBuilder(expression
).build());
372 ScoredDocument
.Builder scoredDocBuilder
= ScoredDocument
.newBuilder(result
.getDocument());
373 for (Double score
: result
.getScoreList()) {
374 scoredDocBuilder
.addScore(score
);
376 for (Field expression
: expressions
) {
377 scoredDocBuilder
.addExpression(expression
);
379 if (result
.hasCursor()) {
380 scoredDocBuilder
.setCursor(
381 Cursor
.newBuilder().build("true:" + result
.getCursor()));
383 scoredDocs
.add(scoredDocBuilder
.build());
385 List
<FacetResult
> facetResults
= new ArrayList
<>();
386 for (SearchServicePb
.FacetResult result
: response
.getFacetResultList()) {
387 facetResults
.add(FacetResult
.newBuilder(result
).build());
389 Results
<ScoredDocument
> scoredResults
= new Results
<>(
390 new OperationResult(status
),
391 scoredDocs
, response
.getMatchedCount(), response
.getResultCount(),
392 (response
.hasCursor() ? Cursor
.newBuilder().build("false:" + response
.getCursor())
393 : null), facetResults
);
394 return scoredResults
;
400 public Future
<Results
<ScoredDocument
>> searchAsync(String query
) {
401 return searchAsync(Query
.newBuilder().build(
402 Preconditions
.checkNotNull(query
, "query cannot be null")));
406 public Future
<Results
<ScoredDocument
>> searchAsync(Query query
) {
407 Preconditions
.checkNotNull(query
, "query cannot be null");
408 return executeSearchForResults(
409 query
.copyToProtocolBuffer().setIndexSpec(spec
.copyToProtocolBuffer(
410 config
.getNamespace())));
414 public Future
<GetResponse
<Document
>> getRangeAsync(GetRequest
.Builder builder
) {
415 return getRangeAsync(builder
.build());
419 public Future
<GetResponse
<Document
>> getRangeAsync(GetRequest request
) {
420 Preconditions
.checkNotNull(request
, "list documents request cannot be null");
422 SearchServicePb
.ListDocumentsParams
.Builder params
=
423 request
.copyToProtocolBuffer().setIndexSpec(spec
.copyToProtocolBuffer(
424 config
.getNamespace()));
426 Future
<SearchServicePb
.ListDocumentsResponse
.Builder
> future
=
427 apiHelper
.makeAsyncListDocumentsCall(params
, config
.getDeadline());
428 return new FutureWrapper
<SearchServicePb
.ListDocumentsResponse
.Builder
,
429 GetResponse
<Document
>>(future
) {
431 protected Throwable
convertException(Throwable cause
) {
432 OperationResult result
= OperationResult
.convertToOperationResult(cause
);
433 return (result
== null) ? cause
: new GetException(result
);
437 protected GetResponse
<Document
> wrap(
438 SearchServicePb
.ListDocumentsResponse
.Builder key
) throws Exception
{
439 SearchServicePb
.ListDocumentsResponse response
= key
.build();
440 SearchServicePb
.RequestStatus status
= response
.getStatus();
442 if (status
.getCode() != ErrorCode
.OK
) {
443 throw new GetException(new OperationResult(status
));
446 List
<Document
> results
= new ArrayList
<>();
447 for (DocumentPb
.Document document
: response
.getDocumentList()) {
448 results
.add(Document
.newBuilder(document
).build());
450 return new GetResponse
<>(results
);
456 public Document
get(String documentId
) {
457 Preconditions
.checkNotNull(documentId
, "documentId must not be null");
458 GetResponse
<Document
> response
=
459 getRange(GetRequest
.newBuilder().setStartId(documentId
).setLimit(1));
460 for (Document document
: response
) {
461 if (documentId
.equals(document
.getId())) {
470 public void deleteSchema() {
471 quietGet(deleteSchemaAsync());
475 public void delete(String
... documentIds
) {
476 quietGet(deleteAsync(documentIds
));
480 public void delete(Iterable
<String
> documentIds
) {
481 quietGet(deleteAsync(documentIds
));
485 public PutResponse
put(Document
... documents
) {
486 return quietGet(putAsync(documents
));
490 public PutResponse
put(Document
.Builder
... builders
) {
491 return quietGet(putAsync(builders
));
495 public PutResponse
put(Iterable
<Document
> documents
) {
496 return quietGet(putAsync(documents
));
500 public Results
<ScoredDocument
> search(String query
) {
501 return quietGet(searchAsync(query
));
505 public Results
<ScoredDocument
> search(Query query
) {
506 return quietGet(searchAsync(query
));
510 public GetResponse
<Document
> getRange(GetRequest request
) {
511 return quietGet(getRangeAsync(request
));
515 public GetResponse
<Document
> getRange(GetRequest
.Builder builder
) {
516 return getRange(builder
.build());