Merge lp:~reedobrien/charmworld/es-migration into lp:charmworld

Proposed by Reed O'Brien
Status: Merged
Approved by: Reed O'Brien
Approved revision: 517
Merged at revision: 512
Proposed branch: lp:~reedobrien/charmworld/es-migration
Merge into: lp:charmworld
Diff against target: 370 lines (+281/-41)
5 files modified
charmworld/migrations/versions/027_use_ngrams.py (+35/-0)
charmworld/migrations/versions/tests/__init__.py (+1/-0)
charmworld/migrations/versions/tests/data/migration_027.py (+166/-0)
charmworld/migrations/versions/tests/test_migrations.py (+79/-4)
charmworld/search.py (+0/-37)
To merge this branch: bzr merge lp:~reedobrien/charmworld/es-migration
Reviewer Review Type Date Requested Status
Juju Gui Bot continuous-integration Approve
Brad Crittenden (community) Approve
Review via email: mp+220144@code.launchpad.net

Commit message

This makes the ngram work migrate cleanly.

Description of the change

Hopefully this makes the ngram work migrate cleanly.

QA:

Go back to r508

$ bzr revert -r 508

Drop the version and delte the index:

$ mongo juju --eval "db.migration_version.drop()"
$ curl -XDELETE http://localhost:9200/charms?pretty=true

Create the r508 index:

$ bin/es-update

Setup the migration system and update to r508 version (026)

$ bin/migrations current
$ bin/migrations prepare-upgrade -i
$ bin/migrations upgrade
$ bin/migrations latest

Reindex the stuff...without ngrams

$ bin/sync-index

Go back to tip (r515)

$ bzr revert

Do the migration...

$ bin/migrations latest
$ bin/migrations current
$ bin/es-update
$ bin/migrations upgrade

Verify it worked

$ curl localhost:9200/charms/_count?pretty=true
$ curl localhost:9200/charms/charm/_count?q=ngrams:crm\&pretty=true&& echo
$ curl localhost:9200/charms/charm/_count?q=ngrams:couch\&pretty=true&& echo

Make sure the tests all pass...

$ make test

To post a comment you must log in.
516. By Reed O'Brien

Remove unused bits of the exists in index helper method.

Revision history for this message
Brad Crittenden (bac) wrote :

Reed this branch looks great, modulo some stylistic issues.

The QA steps you provide are fantastic. The last two steps do make assumptions about what charms have been ingested locally.

Note this review is based on the version of your code as of 8:00am, not the branch you subsequently pushed.

I've marked the branch as approved, which means you are free to land after making (or discussing) the suggested changes. Had I wanted to see the changes made before you land I would've used 'needs fixing' but I don't, so I didn't.

Thanks for persevering and getting this in so we can do the deploy.

review: Approve (bac)
Revision history for this message
Brad Crittenden (bac) wrote :

Oh, and do see the in-line comments!

Revision history for this message
Brad Crittenden (bac) wrote :

Actually you should pass the kind in the exists method:

    def exists_in_index(self, id_, kind=CHARM):
        client = self.index_client
        return client.get(id_, kind) is not None

Revision history for this message
Reed O'Brien (reedobrien) wrote :

> Reed this branch looks great, modulo some stylistic issues.
>
> The QA steps you provide are fantastic. The last two steps do make
> assumptions about what charms have been ingested locally.

Thanks. I thought I had put some ingest bits at the top. I missed that I guess -- nearly fantastic...

> I've marked the branch as approved, which means you are free to land after
> making (or discussing) the suggested changes. Had I wanted to see the changes
> made before you land I would've used 'needs fixing' but I don't, so I didn't.

I'll resubmit for sanity check.

Revision history for this message
Reed O'Brien (reedobrien) wrote :

> Oh, and do see the in-line comments!

Waiting for them to show here. But will work from email until they do.

Revision history for this message
Reed O'Brien (reedobrien) wrote :

> Actually you should pass the kind in the exists method:
>
> def exists_in_index(self, id_, kind=CHARM):
> client = self.index_client
> return client.get(id_, kind) is not None

This shrank to:

def exists_in_index(self, id_):
    return self.index_client.get(id_) is not None

in this test I am only inserting a charm, so adding a specifier seems like noise. If adding a charm let's you find something else, there is a problem and that should be tested elsewhere, IMO. Unless we are aiming to test that bundles upgrade as well, which AFAIU we are not.

517. By Reed O'Brien

Assuages stylistic deviations.

Revision history for this message
Reed O'Brien (reedobrien) wrote :
Download full text (6.0 KiB)

On Tue, May 20, 2014 at 9:04 AM, Brad Crittenden <email address hidden> wrote:

> Review: Approve bac
>
> Reed this branch looks great, modulo some stylistic issues.
>
> The QA steps you provide are fantastic. The last two steps do make
> assumptions about what charms have been ingested locally.
>
> Note this review is based on the version of your code as of 8:00am, not
> the branch you subsequently pushed.
>
> I've marked the branch as approved, which means you are free to land after
> making (or discussing) the suggested changes. Had I wanted to see the
> changes made before you land I would've used 'needs fixing' but I don't, so
> I didn't.
>
> Thanks for persevering and getting this in so we can do the deploy.
>
> Inline comments:
>
>
> > + except IndexMissing:
> > + # for whatever reason it is updated already
> > + pass
>
> Remember to Capitalize and punctuate comments.
>

Done.

> > --- charmworld/migrations/versions/tests/__init__.py 1970-01-01
> 00:00:00 +0000
> > +++ charmworld/migrations/versions/tests/__init__.py 2014-05-20
> 01:08:32 +0000
> > @@ -0,0 +1,1 @@
> > +# a package
> >
>
> I'd actually prefer to see a totally empty file than just this comment.
> See
>

Emptied. This is a vestigial habit from some old VCS that wouldn't commit
empty files and directories.
I can't even remember what that was... Removed.

> > +old_charm_mapping = {"charm": {
> > + "type": "object",
>
> Thanks for figuring out the old mapping structure to provide the test.
> Makes for much more thorough testing.
>

Getting the mapping was easy. Formatting it and converting to python were
less so.

> > === modified file
> 'charmworld/migrations/versions/tests/test_migrations.py'
> > --- charmworld/migrations/versions/tests/test_migrations.py 2014-04-22
> 20:36:12 +0000
> > +
> > +from charmworld.search import CHARM
> > +
>
> If you import BUNDLE too you can use them instead of strings, like at line
> 304.
>

I am actually not importing CHARM at this point either. See other comment
here.

>
> > +from pyelasticsearch.exceptions import (
> > + ElasticHttpError,
> > + ElasticHttpNotFoundError,
> > +)
> >
> > import logging
> > import mock
> > @@ -126,3 +138,79 @@
> > def test_exodus_restrictions(self):
> > """Ensure that deploy-from-scratch does not violate exodus
> rules."""
> > self.versions._check_exodus(self.versions.list_pending(0))
> > +
> > +
> > +class TestUseNgrams027(MigrationTestBase):
> > +
> > + version = 27
> > + module_name = '027_use_ngrams.py'
> > +
> > + def setUp(self):
> > + super(TestUseNgrams027, self).setUp()
> > + self.use_index_client()
> > + self.ds = DataStore(self.db)
> > + self.cs = CharmSource(self.db, self.index_client)
>
> If this were long-lived code I'd suggest more explicit names. We tend to
> the follow the Python guide that code is to be read many more times than
> written so more readable names are better at the sake of brevity.
>

Embiggened.

> > +
> > + def exists_in_index(self, id_, kind=CHARM):
> > + client = self.index_client
> > + if kind == CHARM:
> > + return client.get(id_) is not None
> > +...

Read more...

Revision history for this message
Juju Gui Bot (juju-gui-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'charmworld/migrations/versions/027_use_ngrams.py'
2--- charmworld/migrations/versions/027_use_ngrams.py 1970-01-01 00:00:00 +0000
3+++ charmworld/migrations/versions/027_use_ngrams.py 2014-05-20 14:13:54 +0000
4@@ -0,0 +1,35 @@
5+# use ngrams
6+import logging
7+
8+from charmworld.models import CharmSource
9+
10+from charmworld.search import (
11+ IndexMissing,
12+ update,
13+)
14+
15+
16+def upgrade(db, index_client):
17+ """db is the pymongo db instance for our datastore. Charms are in db.charms
18+ for instance.
19+
20+ index_client is the ElasticSearch client.
21+ """
22+ log = logging.getLogger('migration_027')
23+ log.warning('Starting')
24+ charm_source = CharmSource(db, index_client)
25+ try:
26+ index_client.delete_index()
27+ log.warning("deleted index")
28+ except IndexMissing:
29+ # It is already updated.
30+ pass
31+ log.warning("Index already deleted")
32+ update(index_client)
33+ cids = charm_source.sync_index()
34+ while True:
35+ try:
36+ next(cids)
37+ except StopIteration:
38+ break
39+ log.warning("Index updated")
40
41=== added file 'charmworld/migrations/versions/tests/__init__.py'
42--- charmworld/migrations/versions/tests/__init__.py 1970-01-01 00:00:00 +0000
43+++ charmworld/migrations/versions/tests/__init__.py 2014-05-20 14:13:54 +0000
44@@ -0,0 +1,1 @@
45+# a package
46
47=== added directory 'charmworld/migrations/versions/tests/data'
48=== added file 'charmworld/migrations/versions/tests/data/__init__.py'
49=== added file 'charmworld/migrations/versions/tests/data/migration_027.py'
50--- charmworld/migrations/versions/tests/data/migration_027.py 1970-01-01 00:00:00 +0000
51+++ charmworld/migrations/versions/tests/data/migration_027.py 2014-05-20 14:13:54 +0000
52@@ -0,0 +1,166 @@
53+old_charm_mapping = {"charm": {
54+ "type": "object",
55+ "dynamic": "false",
56+ "properties": {
57+ "data": {
58+ "properties": {
59+ "_id": {
60+ "type": "string",
61+ "index": "not_analyzed",
62+ "omit_norms": True,
63+ "index_options": "docs"
64+ },
65+ "categories": {
66+ "type": "string",
67+ "index": "not_analyzed",
68+ "omit_norms": True,
69+ "index_options": "docs"
70+ },
71+ "config": {
72+ "properties": {
73+ "options": {
74+ "properties": {
75+ "default": {
76+ "type": "string"},
77+ "description": {
78+ "type": "string"}
79+ }
80+ }
81+ }
82+ },
83+ "date_created": {
84+ "type": "date",
85+ "format": "dateOptionalTime"
86+ },
87+ "description": {"type": "string"},
88+ "downloads": {"type": "long"},
89+ "downloads_in_past_30_days": {"type": "long"},
90+ "downloads_in_past_7_days": {"type": "long"},
91+ "downloads_in_past_half_year": {"type": "long"},
92+ "i_provides": {
93+ "type": "string",
94+ "index": "not_analyzed",
95+ "omit_norms": True,
96+ "index_options": "docs"
97+ },
98+ "i_requires": {
99+ "type": "string",
100+ "index": "not_analyzed",
101+ "omit_norms": True,
102+ "index_options": "docs"
103+ },
104+ "is_featured": {"type": "boolean"},
105+ "name": {
106+ "type": "string",
107+ "index": "not_analyzed",
108+ "omit_norms": True,
109+ "index_options": "docs"
110+ },
111+ "owner": {
112+ "type": "string",
113+ "index": "not_analyzed", "omit_norms": True,
114+ "index_options": "docs"
115+ },
116+ "series": {
117+ "type": "string",
118+ "index": "not_analyzed",
119+ "omit_norms": True,
120+ "index_options": "docs"
121+ },
122+ "store_data": {
123+ "properties": {
124+ "errors": {"type": "string"}
125+ }
126+ },
127+ "store_url": {
128+ "type": "string",
129+ "index": "not_analyzed",
130+ "omit_norms": True,
131+ "index_options": "docs"
132+ },
133+ "summary": {"type": "string"}
134+ }
135+ },
136+ "provides": {
137+ "dynamic": "false",
138+ "properties": {
139+ "interface": {
140+ "type": "string",
141+ "index": "not_analyzed",
142+ "omit_norms": True,
143+ "index_options": "docs"
144+ },
145+ "relation_name": {
146+ "type": "string",
147+ "index": "not_analyzed",
148+ "omit_norms": True,
149+ "index_options": "docs"
150+ },
151+ "scope": {
152+ "type": "string",
153+ "index": "not_analyzed",
154+ "omit_norms": True,
155+ "index_options": "docs"
156+ }
157+ }
158+ },
159+ "requires": {
160+ "dynamic": "false",
161+ "properties": {
162+ "interface": {
163+ "type": "string",
164+ "index": "not_analyzed",
165+ "omit_norms": True,
166+ "index_options": "docs"
167+ },
168+ "relation_name": {
169+ "type": "string",
170+ "index": "not_analyzed",
171+ "omit_norms": True,
172+ "index_options": "docs"
173+ },
174+ "scope": {
175+ "type": "string",
176+ "index": "not_analyzed",
177+ "omit_norms": True,
178+ "index_options": "docs"
179+ }
180+ }
181+ }
182+ }
183+}}
184+
185+old_bundle_mapping = {"bundle": {
186+ "type": "object",
187+ "dynamic": "false",
188+ "properties": {
189+ "data": {
190+ "properties": {
191+ "name": {
192+ "type": "string",
193+ "index": "not_analyzed",
194+ "omit_norms": True,
195+ "index_options": "docs"
196+ },
197+ "owner": {
198+ "type": "string",
199+ "index": "not_analyzed",
200+ "omit_norms": True,
201+ "index_options": "docs"
202+ },
203+ "series": {
204+ "type": "string",
205+ "index": "not_analyzed",
206+ "omit_norms": True,
207+ "index_options": "docs"
208+ },
209+ "title": {
210+ "type": "string",
211+ "index": "not_analyzed",
212+ "omit_norms": True,
213+ "index_options": "docs"
214+ }
215+ }
216+ }
217+ }
218+}}
219
220=== modified file 'charmworld/migrations/versions/tests/test_migrations.py'
221--- charmworld/migrations/versions/tests/test_migrations.py 2014-04-22 20:36:12 +0000
222+++ charmworld/migrations/versions/tests/test_migrations.py 2014-05-20 14:13:54 +0000
223@@ -1,16 +1,22 @@
224 # Copyright 2012, 2013 Canonical Ltd. This software is licensed under the
225 # GNU Affero General Public License version 3 (see the file LICENSE).
226
227-from charmworld.models import (
228- CharmSource,
229-)
230 from charmworld.testing import (
231 factory,
232 MongoTestBase,
233 )
234
235 from charmworld.migrations import migrate
236-from charmworld.migrations.migrate import Versions
237+from charmworld.migrations.migrate import (
238+ DataStore,
239+ Versions
240+)
241+
242+from charmworld.migrations.versions.tests.data import migration_027 as mdata
243+
244+from charmworld.models import CharmSource
245+
246+from pyelasticsearch.exceptions import ElasticHttpError
247
248 import logging
249 import mock
250@@ -126,3 +132,72 @@
251 def test_exodus_restrictions(self):
252 """Ensure that deploy-from-scratch does not violate exodus rules."""
253 self.versions._check_exodus(self.versions.list_pending(0))
254+
255+
256+class TestUseNgrams027(MigrationTestBase):
257+
258+ version = 27
259+ module_name = '027_use_ngrams.py'
260+
261+ def setUp(self):
262+ super(TestUseNgrams027, self).setUp()
263+ self.use_index_client()
264+ self.data_store = DataStore(self.db)
265+ self.charm_source = CharmSource(self.db, self.index_client)
266+
267+ def exists_in_index(self, id_):
268+ return self.index_client.get(id_) is not None
269+
270+ def sync_index(self):
271+ charm_ids = self.charm_source.sync_index()
272+ while True:
273+ try:
274+ next(charm_ids)
275+ except StopIteration:
276+ break
277+
278+ def put_old_mapping(self):
279+ # Delete the temp_index.
280+ self.index_client.delete_index()
281+ # Create an empty index.
282+ self.index_client._client.create_index(self.index_client.index_name)
283+ # Put up the old mappings.
284+ for type_ in ('charm', 'bundle'):
285+ self.index_client._client.put_mapping(
286+ self.index_client.index_name,
287+ type_,
288+ getattr(mdata, 'old_{}_mapping'.format(type_)))
289+
290+ def test_index_updated(self):
291+ """Ensure that running upgrade reindexes charms."""
292+ self.put_old_mapping()
293+ self.versions.ensure_initialized(self.data_store, True)
294+ ## Pretend upgrades through 26 are complete.
295+ self.data_store.update_version(26)
296+ charm = factory.get_charm_json()
297+ self.db.charms.insert(charm)
298+ self.assertFalse(self.exists_in_index(charm['_id']))
299+ self.sync_index()
300+ self.assertTrue(self.exists_in_index(charm['_id']))
301+ self.versions.upgrade(self.data_store, self.index_client, True)
302+ self.assertEquals(self.data_store.current_version, self.version)
303+ self.assertTrue(self.exists_in_index(charm['_id']))
304+
305+ def test_exception_raised(self):
306+ """Putting new mapping without creating the filter and analyzer
307+ raises the expected exception.
308+ """
309+ self.versions.ensure_initialized(self.data_store, True)
310+ ## Pretend upgrades through 26 are complete.
311+ self.data_store.update_version(26)
312+ self.put_old_mapping()
313+ charm = factory.get_charm_json()
314+ self.assertFalse(self.exists_in_index(charm['_id']))
315+ self.index_client.index_charm(charm)
316+ self.assertTrue(self.exists_in_index(charm['_id']))
317+ with self.assertRaises(ElasticHttpError) as e:
318+ self.index_client.put_mapping()
319+ self.assertEquals(e.status_code, 400)
320+ self.assertEquals(e.error,
321+ u'MapperParsingException[Analyzer [n3_20grams] '
322+ u'not found for field [ngrams]]')
323
324=== modified file 'charmworld/search.py'
325--- charmworld/search.py 2014-05-15 20:36:22 +0000
326+++ charmworld/search.py 2014-05-20 14:13:54 +0000
327@@ -180,43 +180,6 @@
328 ]
329 }
330 }
331- },
332- "mappings": {
333- "charm": {
334- "properties": {
335- "name": {
336- "type": "multi_field",
337- "fields": {
338- "ngrams": {
339- "type": "string",
340- "analyzer": "n3_20grams"
341- },
342- "name": {
343- "type": "string",
344- "index": "not_analyzed"
345- }
346- }
347- }
348- }
349-
350- },
351- "bundle": {
352- "properties": {
353- "name": {
354- "type": "multi_field",
355- "fields": {
356- "ngrams": {
357- "type": "string",
358- "analyzer": "n3_20grams"
359- },
360- "name": {
361- "type": "string",
362- "index": "not_analyzed"
363- }
364- }
365- }
366- }
367- }
368 }
369 }
370 })

Subscribers

People subscribed via source and target branches

to all changes: