Merge lp:~benji/charmworld/doctype-search into lp:charmworld

Proposed by Benji York
Status: Rejected
Rejected by: Richard Harding
Proposed branch: lp:~benji/charmworld/doctype-search
Merge into: lp:charmworld
Diff against target: 237 lines (+149/-10)
2 files modified
charmworld/search.py (+26/-10)
charmworld/tests/test_search.py (+123/-0)
To merge this branch: bzr merge lp:~benji/charmworld/doctype-search
Reviewer Review Type Date Requested Status
Richard Harding Approve
Juju Gui Bot continuous-integration Pending
Review via email: mp+207738@code.launchpad.net

Commit message

Add "charms:" and "bundles:" prefix to search (both via the web interface and the API).

Description of the change

Add "charms:" and "bundles:" prefix to search (both via the web interface and the API).

To post a comment you must log in.
lp:~benji/charmworld/doctype-search updated
488. By Benji York

fix indentation

Revision history for this message
Richard Harding (rharding) wrote :

Code looks great. Will QA.

Revision history for this message
Richard Harding (rharding) wrote :

Thanks for the change. LGTM and qa-ok.

review: Approve

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmworld/search.py'
2--- charmworld/search.py 2013-11-19 17:26:23 +0000
3+++ charmworld/search.py 2014-02-21 20:41:09 +0000
4@@ -104,6 +104,20 @@
5 unichr(c) if unichr(c) not in _BAD_CHARS else u' ' for c in range(128)]
6 _BAD_CHAR_TABLE = u''.join(_BAD_CHAR_TABLE)
7 _BAD_TRAILING_CHARS = r'\!:'
8+KIND_RE = re.compile('^\s*(bundle|charm)s?(?::(.+))?')
9+
10+
11+def extract_doctype_from_search_terms(terms, doctype=None):
12+ """A search string with a specified doctype overrides the given doctype.
13+
14+ The doctype syntax is of the form "charm:apache2".
15+ """
16+ match = KIND_RE.match(terms)
17+ if match:
18+ doctype = match.group(1)
19+ terms = match.group(2) or ''
20+
21+ return terms, doctype
22
23
24 class ElasticSearchClient:
25@@ -393,7 +407,9 @@
26 for field, boost in bundle_free_text_fields.items()]
27 bundle_fields.extend(bundle_exact_fields)
28
29- if text != '':
30+ if text == '':
31+ dsl = {'match_all': {}}
32+ else:
33 def make_query(data_fields, other_fields=[]):
34 fields = ['data.' + field for field in data_fields]
35 fields.extend(other_fields)
36@@ -414,8 +430,6 @@
37 }}
38 # Union the bundle_dsl and charm_dsl results
39 dsl = {'bool': {'should': [bundle_dsl, charm_dsl]}}
40- else:
41- dsl = {'match_all': {}}
42 return dsl
43
44 @classmethod
45@@ -555,6 +569,7 @@
46 'charm', 'bundle', or None. If None, do not filter.
47 return: A list of {doctype=<charm|bundle>, data=entity}.
48 """
49+ text, doctype = extract_doctype_from_search_terms(text, doctype)
50 if filters is None:
51 filters = {}
52 dsl = self._get_text_query(text, autocomplete)
53@@ -591,13 +606,13 @@
54 Bundle,
55 Charm,
56 )
57- size = None
58- if terms == '':
59- size = 0
60+ doctype = None
61+ if isinstance(terms, basestring):
62+ terms, doctype = extract_doctype_from_search_terms(terms)
63
64 dsl = self._get_text_query(terms)
65 dsl = self._get_official_boost(dsl)
66- dsl = self._get_filtered(dsl, {}, None, valid_only, doctype=None)
67+ dsl = self._get_filtered(dsl, {}, None, valid_only, doctype=doctype)
68
69 with Timer() as timer:
70 status = self.get_status()
71@@ -614,10 +629,11 @@
72 log.info(message + ' Raising IndexNotReady.')
73 raise IndexNotReady(message)
74 result_total = docs['num_docs']
75- if size is None:
76- size = result_total
77- hits = self._client.search(dsl, index=self.index_name, size=size)
78+ hits = self._client.search(
79+ dsl, index=self.index_name, size=result_total)
80 hitlist = hits['hits']['hits']
81+ charms = []
82+ bundles = []
83 charms = [{'data': Charm(hit['_source']['data']),
84 'weight': hit['_score']}
85 for hit in hitlist if hit['_type'] == CHARM]
86
87=== modified file 'charmworld/tests/test_search.py'
88--- charmworld/tests/test_search.py 2013-11-15 20:00:35 +0000
89+++ charmworld/tests/test_search.py 2014-02-21 20:41:09 +0000
90@@ -28,6 +28,7 @@
91 BUNDLE,
92 CHARM,
93 ElasticSearchClient,
94+ extract_doctype_from_search_terms,
95 charm_exact_fields,
96 charm_free_text_fields,
97 IncompatibleMapping,
98@@ -244,6 +245,67 @@
99 self.assertEqual(charm, results['charm'][0]['data'])
100 self.assertEqual(bundle, results['bundle'][0]['data'])
101
102+ def test_searching_for_bundles_only(self):
103+ Charm(self.makeCharm(description='a mozilla charm'))
104+ bundle = Bundle(self.makeBundle(description='a mozilla bundle'))
105+ client = self.index_client
106+ for prefix in ('bundle', 'bundles'):
107+ term = '{}:mozilla'.format(prefix)
108+ search_results = client.search(term)
109+ results = search_results['results']
110+ # Results still has entries for charms and bundles.
111+ self.assertEqual(2, len(results))
112+ self.assertEqual(2, search_results['result_total'])
113+ self.assertEqual(1, search_results['matches'])
114+ self.assertEqual(0, len(results['charm']))
115+ self.assertEqual(1, len(results['bundle']))
116+ self.assertEqual(bundle, results['bundle'][0]['data'])
117+
118+ def test_searching_for_all_bundles(self):
119+ Bundle(self.makeBundle(description='A'))
120+ Bundle(self.makeBundle(description='B'))
121+ Charm(self.makeCharm(description='C'))
122+ client = self.index_client
123+ # Both the singular and plural forms of the keyword work.
124+ for search_term in ('bundle', 'bundles'):
125+ search_results = client.search(search_term)
126+ results = search_results['results']
127+ # There were two matches, both of which were bundles.
128+ self.assertEqual(2, search_results['matches'])
129+ self.assertEqual(2, len(results['bundle']))
130+ self.assertEqual(0, len(results['charm']))
131+
132+ def test_searching_for_charms_only(self):
133+ charm = Charm(self.makeCharm(description='a mozilla charm'))
134+ Bundle(self.makeBundle(description='a mozilla bundle'))
135+ client = self.index_client
136+ for prefix in ('charm', 'charms'):
137+ term = '{}:mozilla'.format(prefix)
138+ search_results = client.search(term)
139+ search_results = client.search('charm: mozilla')
140+ results = search_results['results']
141+ # Results still has entries for charms and bundles.
142+ self.assertEqual(2, len(results))
143+ self.assertEqual(2, search_results['result_total'])
144+ self.assertEqual(1, search_results['matches'])
145+ self.assertEqual(1, len(results['charm']))
146+ self.assertEqual(0, len(results['bundle']))
147+ self.assertEqual(charm, results['charm'][0]['data'])
148+
149+ def test_searching_for_all_charms(self):
150+ Charm(self.makeCharm(description='A'))
151+ Charm(self.makeCharm(description='B'))
152+ Bundle(self.makeBundle(description='C'))
153+ client = self.index_client
154+ # Both the singular and plural forms of the keyword work.
155+ for search_term in ('charm', 'charms'):
156+ search_results = client.search(search_term)
157+ results = search_results['results']
158+ # There were two matches, both of which were charms.
159+ self.assertEqual(2, search_results['matches'])
160+ self.assertEqual(2, len(results['charm']))
161+ self.assertEqual(0, len(results['bundle']))
162+
163 def test_search_not_ready(self):
164
165 class FakeElasticSearch:
166@@ -555,6 +617,22 @@
167 self.assertEqual(1, len(charms))
168 self.assertEqual(abc_charm['_id'], charms[0]['_id'])
169
170+ def test_charm_type_search(self):
171+ # We can ask for all the charms and get them.
172+ self.makeCharm()
173+ self.makeCharm()
174+ self.makeBundle()
175+ results = self.get_charms_and_bundles(text='charms', doctype=None)
176+ self.assertEqual(2, len(results))
177+
178+ def test_bundle_type_search(self):
179+ # We can ask for all the charms and get them.
180+ self.makeCharm()
181+ self.makeBundle()
182+ self.makeBundle()
183+ results = self.get_charms_and_bundles(text='bundles', doctype=None)
184+ self.assertEqual(2, len(results))
185+
186 def test_charms_text_search_respects_filters(self):
187 official = self.makeCharm(summary='abc def', promulgated=True)
188 self.makeCharm(promulgated=True)
189@@ -1121,3 +1199,48 @@
190 # exist.
191 new_client = reindex(client, charms=[])
192 new_client.delete_index()
193+
194+
195+class TestDoctypeExtraction(TestCase):
196+ """Specially-formatted search strings can filter by doctype.
197+
198+ If the search terms start with "bundles:" or "charms:" (with or without
199+ the "s"), then the results are limited to items of that type. The search
200+ text "charms" or "bundles" (again, singular or plural) return all items of
201+ that type.
202+ """
203+
204+ def test_selecting_all_charms(self):
205+ # If the query string only asks for charms, then...
206+ terms, doctype = extract_doctype_from_search_terms('charms')
207+ # ...the doctype reflects the request...
208+ self.assertEqual(doctype, 'charm')
209+ # ...and there are no "real" search terms.
210+ self.assertEqual(terms, '')
211+
212+ def test_selecting_all_charms_singular(self):
213+ # The query string still works without an "s".
214+ self.assertEqual(
215+ extract_doctype_from_search_terms('charm'),
216+ ('', 'charm'))
217+
218+ def test_selecting_all_bundles(self):
219+ # If the query string only asks for bundles, then...
220+ terms, doctype = extract_doctype_from_search_terms('bundles')
221+ # ...the doctype reflects the request...
222+ self.assertEqual(doctype, 'bundle')
223+ # ...and there are no "real" search terms.
224+ self.assertEqual(terms, '')
225+
226+ def test_selecting_all_bundles_singular(self):
227+ # The query string still works without an "s".
228+ self.assertEqual(
229+ extract_doctype_from_search_terms('bundle'),
230+ ('', 'bundle'))
231+
232+ def test_including_additional_search_terms(self):
233+ # If there are non-doctype search terms, they are unmolested.
234+ terms, doctype = extract_doctype_from_search_terms('charms:foo bar baz')
235+ self.assertEqual(terms, 'foo bar baz')
236+ # The doctype was still extracted correctly though.
237+ self.assertEqual(doctype, 'charm')

Subscribers

People subscribed via source and target branches

to all changes: