From 0e5ffac24255903454db8d633765b7c51155c14e Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 14 Oct 2015 12:10:57 -0400 Subject: [PATCH 1/3] Add 'Index.search' API wrapper. --- gcloud/search/index.py | 90 +++++++++++++++++++++++++++ gcloud/search/test_index.py | 117 +++++++++++++++++++++++++++++++++--- 2 files changed, 199 insertions(+), 8 deletions(-) diff --git a/gcloud/search/index.py b/gcloud/search/index.py index 0dc979a740c1..1791bc161472 100644 --- a/gcloud/search/index.py +++ b/gcloud/search/index.py @@ -212,3 +212,93 @@ def document(self, name, rank=None): :returns: a new ``Document`` instance """ return Document(name, index=self, rank=rank) + + def search(self, + query, + max_results=None, + page_token=None, + field_expressions=None, + order_by=None, + matched_count_accuracy=None, + scorer=None, + scorer_size=None, + return_fields=None): + """Search documents created within this index. + + See: + https://cloud.google.com/search/reference/rest/v1/projects/indexes/search + + :type query: string + :param query: query string (see https://cloud.google.com/search/query). + + :type max_results: int + :param max_results: maximum number of zones to return, If not + passed, defaults to a value set by the API. + + :type page_token: string + :param page_token: opaque marker for the next "page" of zones. If + not passed, the API will return the first page of + zones. + + :type field_expressions: dict, or ``NoneType`` + :param field_expressions: mapping of field name -> expression + for use in 'order_by' or 'return_fields' + + :type order_by: sequence of string, or ``NoneType`` + :param order_by: list of field names (plus optional ' desc' suffix) + specifying ordering of results. + + :type matched_count_accuracy: integer or ``NoneType`` + :param matched_count_accuracy: minimum accuracy for matched count + returned + + :type return_fields: sequence of string, or ``NoneType`` + :param return_fields: list of field names to be returned. + + :type scorer: string or ``NoneType`` + :param scorer: name of scorer function (e.g., "generic"). + + :type scorer_size: integer or ``NoneType`` + :param scorer_size: max number of top results pass to scorer function. + + :rtype: tuple, (list, str, int) + :returns: list of :class:`gcloud.dns.document.Document`, plus a + "next page token" string, and a "matched count". If the + token is not None, indicates that more zones can be + retrieved with another call (pass that value as + ``page_token``). The "matched count" indicates the total + number of documents matching the query string. + """ + params = {'query': query} + + if max_results is not None: + params['pageSize'] = max_results + + if page_token is not None: + params['pageToken'] = page_token + + if field_expressions is not None: + params['fieldExpressions'] = field_expressions + + if order_by is not None: + params['orderBy'] = order_by + + if matched_count_accuracy is not None: + params['matchedCountAccuracy'] = matched_count_accuracy + + if scorer is not None: + params['scorer'] = scorer + + if scorer_size is not None: + params['scorerSize'] = scorer_size + + if return_fields is not None: + params['returnFields'] = return_fields + + path = '%s/search' % (self.path,) + connection = self._client.connection + resp = connection.api_request(method='GET', path=path, + query_params=params) + zones = [Document.from_api_repr(resource, self) + for resource in resp['results']] + return zones, resp.get('nextPageToken'), resp.get('matchedCount') diff --git a/gcloud/search/test_index.py b/gcloud/search/test_index.py index 4627bc524e11..40225d1a358d 100644 --- a/gcloud/search/test_index.py +++ b/gcloud/search/test_index.py @@ -79,8 +79,8 @@ def _verifyResourceProperties(self, index, resource): def _verifyDocumentResource(self, documents, resource): from gcloud.search.document import Document from gcloud.search.document import StringValue - self.assertEqual(len(documents), len(resource['documents'])) - for found, expected in zip(documents, resource['documents']): + self.assertEqual(len(documents), len(resource)) + for found, expected in zip(documents, resource): self.assertTrue(isinstance(found, Document)) self.assertEqual(found.name, expected['docId']) self.assertEqual(found.rank, expected.get('rank')) @@ -149,17 +149,17 @@ def test_list_documents_defaults(self): TOKEN = 'TOKEN' DOC_1 = self._makeDocumentResource(DOCID_1) DOC_2 = self._makeDocumentResource(DOCID_2) - DATA = { + RESPONSE = { 'nextPageToken': TOKEN, 'documents': [DOC_1, DOC_2], } client = _Client(self.PROJECT) - conn = client.connection = _Connection(DATA) + conn = client.connection = _Connection(RESPONSE) index = self._makeOne(self.INDEX_ID, client) documents, token = index.list_documents() - self._verifyDocumentResource(documents, DATA) + self._verifyDocumentResource(documents, RESPONSE['documents']) self.assertEqual(token, TOKEN) self.assertEqual(len(conn._requested), 1) @@ -180,15 +180,15 @@ def test_list_documents_explicit(self): TOKEN = 'TOKEN' DOC_1 = self._makeDocumentResource(DOCID_1, RANK_1, TITLE_1) DOC_2 = self._makeDocumentResource(DOCID_2, RANK_2, TITLE_2) - DATA = {'documents': [DOC_1, DOC_2]} + RESPONSE = {'documents': [DOC_1, DOC_2]} client = _Client(self.PROJECT) - conn = client.connection = _Connection(DATA) + conn = client.connection = _Connection(RESPONSE) index = self._makeOne(self.INDEX_ID, client) documents, token = index.list_documents( max_results=3, page_token=TOKEN, view='FULL') - self._verifyDocumentResource(documents, DATA) + self._verifyDocumentResource(documents, RESPONSE['documents']) self.assertEqual(token, None) self.assertEqual(len(conn._requested), 1) @@ -227,6 +227,107 @@ def test_document_explicit(self): self.assertEqual(document.rank, RANK) self.assertTrue(document.index is index) + def test_search_defaults(self): + DOCID_1 = 'docid-one' + TITLE_1 = 'Title One' + DOCID_2 = 'docid-two' + TITLE_2 = 'Title Two' + PATH = 'projects/%s/indexes/%s/search' % ( + self.PROJECT, self.INDEX_ID) + TOKEN = 'TOKEN' + DOC_1 = self._makeDocumentResource(DOCID_1, title=TITLE_1) + DOC_2 = self._makeDocumentResource(DOCID_2, title=TITLE_2) + QUERY = 'query string' + RESPONSE = { + 'nextPageToken': TOKEN, + 'matchedCount': 2, + 'results': [DOC_1, DOC_2], + } + client = _Client(self.PROJECT) + conn = client.connection = _Connection(RESPONSE) + index = self._makeOne(self.INDEX_ID, client) + + documents, token, matched_count = index.search(QUERY) + + self._verifyDocumentResource(documents, RESPONSE['results']) + self.assertEqual(token, TOKEN) + self.assertEqual(matched_count, 2) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], {'query': QUERY}) + + def test_search_explicit(self): + DOCID_1 = 'docid-one' + TITLE_1 = 'Title One' + FUNKY_1 = 'this is a funky show' + RANK_1 = 2345 + DOCID_2 = 'docid-two' + TITLE_2 = 'Title Two' + FUNKY_2 = 'delighfully funky ambiance' + RANK_2 = 1234 + PATH = 'projects/%s/indexes/%s/search' % ( + self.PROJECT, self.INDEX_ID) + TOKEN = 'TOKEN' + + def _makeFunky(text): + return { + 'values': [{ + 'stringValue': text, + 'stringFormat': 'text', + 'lang': 'en', + }] + } + + DOC_1 = self._makeDocumentResource(DOCID_1, RANK_1, TITLE_1) + DOC_1['fields']['funky'] = _makeFunky(FUNKY_1) + DOC_2 = self._makeDocumentResource(DOCID_2, RANK_2, TITLE_2) + DOC_2['fields']['funky'] = _makeFunky(FUNKY_2) + EXPRESSIONS = {'funky': 'snippet("funky", content)'} + QUERY = 'query string' + RESPONSE = { + 'matchedCount': 2, + 'results': [DOC_1, DOC_2], + } + client = _Client(self.PROJECT) + conn = client.connection = _Connection(RESPONSE) + index = self._makeOne(self.INDEX_ID, client) + + documents, token, matched_count = index.search( + query=QUERY, + max_results=3, + page_token=TOKEN, + field_expressions=EXPRESSIONS, + order_by=['title'], + matched_count_accuracy=100, + scorer='generic', + scorer_size=20, + return_fields=['_rank', 'title', 'funky'], + ) + + self._verifyDocumentResource(documents, RESPONSE['results']) + self.assertEqual(token, None) + self.assertEqual(matched_count, 2) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + expected_params = { + 'query': QUERY, + 'pageSize': 3, + 'pageToken': TOKEN, + 'fieldExpressions': EXPRESSIONS, + 'orderBy': ['title'], + 'matchedCountAccuracy': 100, + 'scorer': 'generic', + 'scorerSize': 20, + 'returnFields': ['_rank', 'title', 'funky'], + } + self.assertEqual(req['query_params'], expected_params) + class _Client(object): From 54f7922bc28e6494c697553adf51e32d78b73bf4 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 14 Oct 2015 14:57:35 -0400 Subject: [PATCH 2/3] Replace copy-pasta'ed 'zone' -> 'index'. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1178#discussion_r42034945. --- gcloud/search/client.py | 16 ++++++++-------- gcloud/search/index.py | 24 ++++++++++++------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/gcloud/search/client.py b/gcloud/search/client.py index 757533fc1f21..731ccd14ecf5 100644 --- a/gcloud/search/client.py +++ b/gcloud/search/client.py @@ -25,7 +25,7 @@ class Client(JSONClient): :type project: string :param project: the project which the client acts on behalf of. Will be - passed when creating a zone. If not passed, + passed when creating a index. If not passed, falls back to the default inferred from the environment. :type credentials: :class:`oauth2client.client.OAuth2Credentials` or @@ -45,19 +45,19 @@ class Client(JSONClient): def list_indexes(self, max_results=None, page_token=None, view=None, prefix=None): - """List zones for the project associated with this client. + """List indexes for the project associated with this client. See: https://cloud.google.com/search/reference/rest/v1/indexes/list :type max_results: int - :param max_results: maximum number of zones to return, If not + :param max_results: maximum number of indexes to return, If not passed, defaults to a value set by the API. :type page_token: string - :param page_token: opaque marker for the next "page" of zones. If + :param page_token: opaque marker for the next "page" of indexes. If not passed, the API will return the first page of - zones. + indexes. :type view: string :param view: One of 'ID_ONLY' (return only the index ID; the default) @@ -69,7 +69,7 @@ def list_indexes(self, max_results=None, page_token=None, :rtype: tuple, (list, str) :returns: list of :class:`gcloud.dns.index.Index`, plus a "next page token" string: if the token is not None, - indicates that more zones can be retrieved with another + indicates that more indexes can be retrieved with another call (pass that value as ``page_token``). """ params = {} @@ -89,9 +89,9 @@ def list_indexes(self, max_results=None, page_token=None, path = '/projects/%s/indexes' % (self.project,) resp = self.connection.api_request(method='GET', path=path, query_params=params) - zones = [Index.from_api_repr(resource, self) + indexes = [Index.from_api_repr(resource, self) for resource in resp['indexes']] - return zones, resp.get('nextPageToken') + return indexes, resp.get('nextPageToken') def index(self, name): """Construct an index bound to this client. diff --git a/gcloud/search/index.py b/gcloud/search/index.py index 1791bc161472..8514607ef48e 100644 --- a/gcloud/search/index.py +++ b/gcloud/search/index.py @@ -159,13 +159,13 @@ def list_documents(self, max_results=None, page_token=None, https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents/list :type max_results: int - :param max_results: maximum number of zones to return, If not + :param max_results: maximum number of indexes to return, If not passed, defaults to a value set by the API. :type page_token: string - :param page_token: opaque marker for the next "page" of zones. If + :param page_token: opaque marker for the next "page" of indexes. If not passed, the API will return the first page of - zones. + indexes. :type view: string :param view: One of 'ID_ONLY' (return only the document ID; the @@ -176,7 +176,7 @@ def list_documents(self, max_results=None, page_token=None, :rtype: tuple, (list, str) :returns: list of :class:`gcloud.dns.document.Document`, plus a "next page token" string: if the token is not None, - indicates that more zones can be retrieved with another + indicates that more indexes can be retrieved with another call (pass that value as ``page_token``). """ params = {} @@ -194,9 +194,9 @@ def list_documents(self, max_results=None, page_token=None, connection = self._client.connection resp = connection.api_request(method='GET', path=path, query_params=params) - zones = [Document.from_api_repr(resource, self) + indexes = [Document.from_api_repr(resource, self) for resource in resp['documents']] - return zones, resp.get('nextPageToken') + return indexes, resp.get('nextPageToken') def document(self, name, rank=None): """Construct a document bound to this index. @@ -232,13 +232,13 @@ def search(self, :param query: query string (see https://cloud.google.com/search/query). :type max_results: int - :param max_results: maximum number of zones to return, If not + :param max_results: maximum number of indexes to return, If not passed, defaults to a value set by the API. :type page_token: string - :param page_token: opaque marker for the next "page" of zones. If + :param page_token: opaque marker for the next "page" of indexes. If not passed, the API will return the first page of - zones. + indexes. :type field_expressions: dict, or ``NoneType`` :param field_expressions: mapping of field name -> expression @@ -264,7 +264,7 @@ def search(self, :rtype: tuple, (list, str, int) :returns: list of :class:`gcloud.dns.document.Document`, plus a "next page token" string, and a "matched count". If the - token is not None, indicates that more zones can be + token is not None, indicates that more indexes can be retrieved with another call (pass that value as ``page_token``). The "matched count" indicates the total number of documents matching the query string. @@ -299,6 +299,6 @@ def search(self, connection = self._client.connection resp = connection.api_request(method='GET', path=path, query_params=params) - zones = [Document.from_api_repr(resource, self) + indexes = [Document.from_api_repr(resource, self) for resource in resp['results']] - return zones, resp.get('nextPageToken'), resp.get('matchedCount') + return indexes, resp.get('nextPageToken'), resp.get('matchedCount') From 562cff4780a23c759514b1c0b44d26c505e23c06 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 14 Oct 2015 15:18:40 -0400 Subject: [PATCH 3/3] Valet indentation. --- gcloud/search/client.py | 2 +- gcloud/search/index.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gcloud/search/client.py b/gcloud/search/client.py index 731ccd14ecf5..451c1ee37a7c 100644 --- a/gcloud/search/client.py +++ b/gcloud/search/client.py @@ -90,7 +90,7 @@ def list_indexes(self, max_results=None, page_token=None, resp = self.connection.api_request(method='GET', path=path, query_params=params) indexes = [Index.from_api_repr(resource, self) - for resource in resp['indexes']] + for resource in resp['indexes']] return indexes, resp.get('nextPageToken') def index(self, name): diff --git a/gcloud/search/index.py b/gcloud/search/index.py index 8514607ef48e..c9014b24817e 100644 --- a/gcloud/search/index.py +++ b/gcloud/search/index.py @@ -195,7 +195,7 @@ def list_documents(self, max_results=None, page_token=None, resp = connection.api_request(method='GET', path=path, query_params=params) indexes = [Document.from_api_repr(resource, self) - for resource in resp['documents']] + for resource in resp['documents']] return indexes, resp.get('nextPageToken') def document(self, name, rank=None): @@ -300,5 +300,5 @@ def search(self, resp = connection.api_request(method='GET', path=path, query_params=params) indexes = [Document.from_api_repr(resource, self) - for resource in resp['results']] + for resource in resp['results']] return indexes, resp.get('nextPageToken'), resp.get('matchedCount')