lexster 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e58713a9965ca82acd9d981adaa2451bbe1c85f4
4
+ data.tar.gz: 336d87ffd60736d34967f33edbd33b51d952d866
5
+ SHA512:
6
+ metadata.gz: 1df55ed5565f0c84e31764481c1fafbfbc608fac158eefe6282c87906dcb7dafd13f0cdba678e66a70559337ee1cdb38e64745a30c51292f34d53fe0a7494075
7
+ data.tar.gz: a1303d614c49e858196be1a8cf33fea1c9408ae2d38a5f6e662b116ee1ddb9f88231dfb13ba1d99f719a27c8180ede559589ff7a3267e4bbe9fbaf5750f69f5d
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,4 @@
1
+ script: "bundle exec rake neo4j:install neo4j:start spec --trace"
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
@@ -0,0 +1,58 @@
1
+ ## v0.1.1
2
+
3
+ * Added batch support, for much faster intiialization of current DB or reindexing all DB.
4
+ * Dropped indexes per model, instead, using `node_auto_index` and `relationship_auto_index`, letting Neo4j auto index objects.
5
+ * One `neo_save` method instead of `neo_create` and `neo_update`. It takes care of inserting or updating.
6
+
7
+ ### Breaking changes:
8
+
9
+ Model indexes (such as `users_index`) are now turned off by default. Instead, Lexster uses Neo4j's auto indexing feature.
10
+
11
+ In order to have the model indexes back, use this in your configuration:
12
+
13
+ ```ruby
14
+ Lexster.configure do |c|
15
+ c.enable_per_model_indexes = true
16
+ end
17
+ ```
18
+
19
+ This will turn on for all models.
20
+
21
+ You can turn off for a specific model with:
22
+
23
+ ```ruby
24
+ class User < ActiveRecord::Base
25
+ include Lexster::Node
26
+
27
+ lexsterable enable_model_index: false do |c|
28
+ end
29
+ end
30
+ ```
31
+
32
+ ## v0.0.51
33
+
34
+ * Releasing Lexster as a gem.
35
+
36
+ ## v0.0.41
37
+
38
+ * fixed really annoying bug caused by Rails design -- Rails doesn't call `after_destroy` when assigning many to many relationships to a model, like `user.movies = [m1, m2, m3]` or `user.update_attributes(params[:user])` where it contains `params[:user][:movie_ids]` list (say from checkboxes), but it DOES CALL after_create for the new relationships. the fix adds after_remove callback to the has_many relationships, ensuring neo4j is up to date with all changes, no matter how they were committed
39
+
40
+ ## v0.0.4
41
+
42
+ * rewrote seacrch. one index for all types instead of one for type. please run neo_search_index on all of your models.
43
+ search in multiple types at once with `Lexster.search(types_array, term)
44
+
45
+ ## v0.0.3
46
+
47
+ * new configuration syntax (backwards compatible)
48
+ * full text search index
49
+
50
+ ## v0.0.2
51
+
52
+ * create node immediately after active record create
53
+ * logging
54
+ * bug fixes
55
+
56
+ ## v0.0.1
57
+
58
+ * initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in lexster.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2012 Elad Ossadon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,467 @@
1
+ # Lexster
2
+
3
+ [![Build Status](https://secure.travis-ci.org/elado/lexster.png)](http://travis-ci.org/elado/lexster)
4
+
5
+
6
+ Make your ActiveRecords stored and searchable on Neo4j graph database, in order to make fast graph queries that MySQL would crawl while doing them.
7
+
8
+ Lexster to Neo4j is like Sunspot to Solr. You get the benefits of Neo4j speed while keeping your schema on your plain old RDBMS.
9
+
10
+ Lexster doesn't require JRuby. It's based on the great [Neography](https://github.com/maxdemarzi/neography) gem which uses Neo4j's REST API.
11
+
12
+ Lexster offers querying Neo4j for IDs of objects and then fetch them from your RDBMS, or storing all desired data on Neo4j.
13
+
14
+ **Important: Heroku Support is not available because Herokud doesn't support Gremlin. So until further notice, easiest way is to self host a Neo4j on EC2 in the same zone, and connect from your dyno to it**
15
+
16
+ ## Changelog
17
+
18
+ [See Changelog](https://github.com/elado/lexster/blob/master/CHANGELOG.md). Including some breaking changes (and solutions) from previos versions.
19
+
20
+
21
+ ## Installation
22
+
23
+ Add to your Gemfile and run the `bundle` command to install it.
24
+
25
+ ```ruby
26
+ gem 'lexster', '~> 0.1.1'
27
+ ```
28
+
29
+ **Requires Ruby 1.9.2 or later.**
30
+
31
+ ## Usage
32
+
33
+ ### Rails app configuration:
34
+
35
+ In an initializer, such as `config/initializers/01_neo4j.rb`:
36
+
37
+ ```ruby
38
+ ENV["NEO4J_URL"] ||= "http://localhost:7474"
39
+
40
+ uri = URI.parse(ENV["NEO4J_URL"])
41
+
42
+ $neo = Neography::Rest.new(uri.to_s)
43
+
44
+ Neography.configure do |c|
45
+ c.server = uri.host
46
+ c.port = uri.port
47
+
48
+ if uri.user && uri.password
49
+ c.authentication = 'basic'
50
+ c.username = uri.user
51
+ c.password = uri.password
52
+ end
53
+ end
54
+
55
+ Lexster.db = $neo
56
+
57
+ Lexster.configure do |c|
58
+ # should Lexster create sub-reference from the ref node (id#0) to every node-model? default: true
59
+ c.enable_subrefs = true
60
+ end
61
+ ```
62
+
63
+ `01_` in the file name is in order to get this file loaded first, before the models (initializers are loaded alphabetically).
64
+
65
+ If you have a better idea (I bet you do!) please let me know.
66
+
67
+
68
+ ### ActiveRecord configuration
69
+
70
+ #### Nodes
71
+
72
+ For nodes, first include the `Lexster::Node` module in your model:
73
+
74
+
75
+ ```ruby
76
+ class User < ActiveRecord::Base
77
+ include Lexster::Node
78
+ end
79
+ ```
80
+
81
+ This will help to create/update/destroy a corresponding node on Neo4j when changed are made a User model.
82
+
83
+ Then, you can customize what fields will be saved on the node in Neo4j, inside `lexsterable` configuration, using `field`. You can also pass blocks to save content that's not a real column:
84
+
85
+ ```ruby
86
+ class User < ActiveRecord::Base
87
+ include Lexster::Node
88
+
89
+ lexsterable do |c|
90
+ c.field :slug
91
+ c.field :display_name
92
+ c.field :display_name_length do
93
+ self.display_name.length
94
+ end
95
+ end
96
+ end
97
+ ```
98
+
99
+ #### Relationships
100
+
101
+ Let's assume that a `User` can `Like` `Movie`s:
102
+
103
+
104
+ ```ruby
105
+ # user.rb
106
+
107
+ class User < ActiveRecord::Base
108
+ include Lexster::Node
109
+
110
+ has_many :likes
111
+ has_many :movies, through: :likes
112
+
113
+ lexsterable do |c|
114
+ c.field :slug
115
+ c.field :display_name
116
+ end
117
+ end
118
+
119
+
120
+ # movie.rb
121
+
122
+ class Movie < ActiveRecord::Base
123
+ include Lexster::Node
124
+
125
+ has_many :likes
126
+ has_many :users, through: :likes
127
+
128
+ lexsterable do |c|
129
+ c.field :slug
130
+ c.field :name
131
+ end
132
+ end
133
+
134
+
135
+ # like.rb
136
+
137
+ class Like < ActiveRecord::Base
138
+ belongs_to :user
139
+ belongs_to :movie
140
+ end
141
+ ```
142
+
143
+
144
+ Now let's make the `Like` model a Lexster, by including the `Lexster::Relationship` module, and define the relationship (start & end nodes and relationship type) options with `lexsterable` config and `relationship` method:
145
+
146
+
147
+ ```ruby
148
+ class Like < ActiveRecord::Base
149
+ belongs_to :user
150
+ belongs_to :movie
151
+
152
+ include Lexster::Relationship
153
+
154
+ lexsterable do |c|
155
+ c.relationship start_node: :user, end_node: :movie, type: :likes
156
+ end
157
+ end
158
+ ```
159
+
160
+ Lexster adds the metohds `neo_node` and `neo_relationships` to instances of nodes and relationships, respectively.
161
+
162
+ So you could do:
163
+
164
+ ```ruby
165
+ user = User.create!(display_name: "elado")
166
+ user.movies << Movie.create("Memento")
167
+ user.movies << Movie.create("Inception")
168
+
169
+ user.neo_node # => #<Neography::Node…>
170
+ user.neo_node.display_name # => "elado"
171
+
172
+ rel = user.likes.first.neo_relationship
173
+ rel.start_node # user.neo_node
174
+ rel.end_node # user.movies.first.neo_node
175
+ rel.rel_type # 'likes'
176
+ ```
177
+
178
+ #### Disabling auto saving to Neo4j:
179
+
180
+ If you'd like to save nodes manually rather than after_save, use `auto_index: false`:
181
+
182
+ ```ruby
183
+ class User < ActiveRecord::Base
184
+ include Lexster::Node
185
+
186
+ lexsterable auto_index: false do |c|
187
+ end
188
+ end
189
+
190
+ user = User.create!(name: "Elad") # no node is created in Neo4j!
191
+
192
+ user.neo_save # now there is!
193
+ ```
194
+
195
+ ## Querying
196
+
197
+ You can query with all [Neography](https://github.com/maxdemarzi/neography)'s API: `traverse`, `execute_query` for Cypher, and `execute_script` for Gremlin.
198
+
199
+ ### Basics:
200
+
201
+ #### Finding a node by ID
202
+
203
+ Nodes and relationships are auto indexed in the `node_auto_index` and `relationship_auto_index` indexes, where the key is `Lexster::UNIQUE_ID_KEY` (which is 'lexster_unique_id') and the value is a combination of the class name and model id, `Movie:43`, this value is accessible with `model.neo_unique_id`. So use the constant and this method, never rely on assebling those values on your own because they might change in the future.
204
+
205
+ That means, you can query like this:
206
+
207
+ ```ruby
208
+ Lexster.db.get_node_auto_index(Lexster::UNIQUE_ID_KEY, user.neo_unique_id)
209
+ # => returns a Neography hash
210
+
211
+ Lexster::Node.from_hash(Lexster.db.get_node_auto_index(Lexster::UNIQUE_ID_KEY, user.neo_unique_id))
212
+ # => returns a Neography::Node
213
+ ```
214
+
215
+ #### Finding all nodes of type
216
+
217
+ If Subreferences are enabled, you can get the subref node and then get all attached nodes:
218
+
219
+ ```ruby
220
+ Lexster.ref_node.outgoing('users_subref').first.outgoing('users').to_a
221
+ # => this, according to Neography, returns an array of Neography::Node so no conversion is needed
222
+ ```
223
+
224
+ ### Gremlin Example:
225
+
226
+ These examples query Neo4j using Gremlin for IDs of objects, and then fetches them from ActiveRecord with an `in` query.
227
+
228
+ Of course, you can store using the `lexsterable do |c| c.field ... end` all the data you need in Neo4j and avoid querying ActiveRecord.
229
+
230
+
231
+ **Most liked movies**
232
+
233
+ ```ruby
234
+ gremlin_query = <<-GREMLIN
235
+ m = [:]
236
+
237
+ g.v(0)
238
+ .out('movies_subref').out
239
+ .inE('likes')
240
+ .inV
241
+ .groupCount(m).iterate()
242
+
243
+ m.sort{-it.value}.collect{it.key.ar_id}
244
+ GREMLIN
245
+
246
+ movie_ids = Lexster.db.execute_script(gremlin_query)
247
+
248
+ Movie.where(id: movie_ids)
249
+ ```
250
+
251
+ *Side note: the resulted movies won't be sorted by like count because the RDBMS won't necessarily do it as we passed a list of IDs. You can sort it yourself with array manipulation, since you have the ids.*
252
+
253
+
254
+ **Movies of user friends that the user doesn't have**
255
+
256
+ Let's assume we have another `Friendship` model which is a relationship with start/end nodes of `user` and type of `friends`,
257
+
258
+ ```ruby
259
+ user = User.find(1)
260
+
261
+ gremlin_query = <<-GREMLIN
262
+ u = g.idx('node_auto_index').get(unique_id_key, user_unique_id).next()
263
+ movies = []
264
+
265
+ u
266
+ .out('likes').aggregate(movies).back(2)
267
+ .out('friends').out('likes')
268
+ .dedup
269
+ .except(movies).collect{it.ar_id}
270
+ GREMLIN
271
+
272
+ movie_ids = Lexster.db.execute_script(gremlin_query, unique_id_key: Lexster::UNIQUE_ID_KEY, user_unique_id: user.neo_unique_id)
273
+
274
+ Movie.where(id: movie_ids)
275
+ ```
276
+
277
+ ## Full Text Search
278
+
279
+ ### Index for Full-Text Search
280
+
281
+ Using `search` block inside a `lexsterable` block, you can store certain fields.
282
+
283
+ ```ruby
284
+ # movie.rb
285
+
286
+ class Movie < ActiveRecord::Base
287
+ include Lexster::Node
288
+
289
+ lexsterable do |c|
290
+ c.field :slug
291
+ c.field :name
292
+
293
+ c.search do |s|
294
+ # full-text index fields
295
+ s.fulltext :name
296
+ s.fulltext :description
297
+
298
+ # just index for exact matches
299
+ s.index :year
300
+ end
301
+ end
302
+ end
303
+ ```
304
+
305
+ Records will be automatically indexed when inserted or updated.
306
+
307
+ ### Querying a Full-Text Search index
308
+
309
+ ```ruby
310
+ # will match all movies with full-text match for name/description. returns ActiveRecord instanced
311
+ Movie.neo_search("*hello*").results
312
+
313
+ # same as above but returns hashes with the values that were indexed on Neo4j
314
+ Movie.search("*hello*").hits
315
+
316
+ # search in multiple types
317
+ Lexster.neo_search([Movie, User], "hello")
318
+
319
+ # search with exact matches (pass a hash of field/value)
320
+ Movie.neo_search(year: 2013).results
321
+ ```
322
+
323
+ Full text search with Lexster is very limited and is likely not to develop more than this basic functionality. I strongly recommend using gems like Sunspot over Solr.
324
+
325
+ ## Batches
326
+
327
+ Lexster has a batch ability, that is good for mass updateing/inserting of nodes/relationships. It sends batched requests to Neography, and takes care of type conversion (neography batch returns hashes and other primitive types) and "after" actions (via promises).
328
+
329
+ A few examples, easy to complex:
330
+
331
+ ```ruby
332
+ Lexster.batch(batch_size: 100) do
333
+ User.all.each(&:neo_save)
334
+ end
335
+ ```
336
+ With `then`:
337
+
338
+ ```ruby
339
+ User.first.name # => "Elad"
340
+
341
+ Lexster.batch(batch_size: 100) do
342
+ User.all.each(&:neo_save)
343
+ end.then do |results|
344
+ # results is an array of the script results from neo4j REST.
345
+
346
+ results[0].name # => "Elad"
347
+ end
348
+ ```
349
+
350
+ *Nodes and relationships in the results are automatically converted to Neography::Node and Neography::Relationship, respectively.*
351
+
352
+ With individual `then` as well as `then` for the entire batch:
353
+
354
+ ```ruby
355
+ Lexster.batch(batch_size: 30) do |batch|
356
+ (1..90).each do |i|
357
+ (batch << [:create_node, { name: "Hello #{i}" }]).then { |result| puts result.name }
358
+ end
359
+ end.then do |results|
360
+ puts results.collect(&:name)
361
+ end
362
+ ```
363
+
364
+ When in a batch, `neo_save` adds gremlin scripts to a batch, instead of running them immediately. The batch flushes whenever the `batch_size` option is met.
365
+ So even if you have 20000 users, Lexster will insert/update in smaller batches. Default `batch_size` is 200.
366
+
367
+
368
+ ## Inserting records of existing app
369
+
370
+ If you have an existing database and just want to integrate Lexster, configure the `lexsterable`s and run in a rake task or console.
371
+
372
+ Use batches! It's free, and much faster. Also, you should use `includes` to incude the relationship edges on relationship entities, so it doesn't query the DB on each relationship.
373
+
374
+ ```ruby
375
+ Lexster.batch do
376
+ [ Like.includes(:user).includes(:movie), OtherRelationshipModel.includes(:from_model).includes(:to_model) ].each { |model| model.all.each(&:neo_save) }
377
+
378
+ NodeModel.all.each(&:neo_save)
379
+ end
380
+ ```
381
+
382
+ This will loop through all of your relationship records and generate the two edge nodes along with a relationship (eager loading for better performance).
383
+ The second line is for nodes without relationships.
384
+
385
+ For large data sets use pagination.
386
+ Better interface for that in the future.
387
+
388
+
389
+ ## Behind The Scenes
390
+
391
+ Whenever the `neo_node` on nodes or `neo_relationship` on relationships is called, Lexster checks if there's a corresponding node/relationship in Neo4j (with the auto indexes). If not, it does the following:
392
+
393
+ ### For Nodes:
394
+
395
+ 1. Ensures there's a sub reference node (read [here](http://docs.neo4j.org/chunked/stable/tutorials-java-embedded-index.html) about sub references), if that option is on.
396
+ 2. Creates a node based on the ActiveRecord, with the `id` attribute and all other attributes from `lexsterable`'s field list
397
+ 3. Creates a relationship between the sub reference node and the newly created node
398
+ 4. Auto indexes a node in the auto index, for fast lookup in the future
399
+
400
+ Then, when it needs to find it again, it just seeks the auto index with that ActiveRecord id.
401
+
402
+ ### For Relationships:
403
+
404
+ Like Nodes, it uses an auto index, to look up a relationship by ActiveRecord id
405
+
406
+ 1. With the options passed in the `lexsterable`, it fetches the `start_node` and `end_node`
407
+ 2. Then, it calls `neo_node` on both, in order to create the Neo4j nodes if they're not created yet, and creates the relationship with the type from the options.
408
+ 3. Adds the relationship to the relationship index.
409
+
410
+ ## Testing
411
+
412
+ In order to test your app or this gem, you need a running Neo4j database, dedicated to tests.
413
+
414
+ I use port 7574 for testing.
415
+
416
+ To run another database locally (read [here](http://docs.neo4j.org/chunked/1.9.M03/server-installation.html#_multiple_server_instances_on_one_machine) too):
417
+
418
+ Copy the entire Neo4j database folder to a different location,
419
+
420
+ **or**
421
+
422
+ symlink `bin`, `lib`, `plugins`, `system`, copy `conf` to a single folder, and create an empty `data` folder.
423
+
424
+ Then, edit `conf/neo4j-server.properties` and set the port (`org.neo4j.server.webserver.port`) from 7474 to 7574 and run the server with `bin/neo4j start`
425
+
426
+ ## Testing Your App with Lexster (RSpec)
427
+
428
+ In `environments/test.rb`, add:
429
+
430
+ ```ruby
431
+ ENV["NEO4J_URL"] = 'http://localhost:7574'
432
+ ```
433
+
434
+ In your `spec_helper.rb`, add the following configurations:
435
+
436
+ ```ruby
437
+ config.before :all do
438
+ Lexster.clean_db(:yes_i_am_sure)
439
+ end
440
+
441
+ config.before :each do
442
+ Lexster.reset_cached_variables
443
+ end
444
+ ```
445
+
446
+ ## Testing This Gem
447
+
448
+ Run the Neo4j DB on port 7574, and run `rake` from the gem folder.
449
+
450
+ ## Contributing
451
+
452
+ Please create a [new issue](https://github.com/elado/lexster/issues) if you run into any bugs. Contribute patches via pull requests. Write tests and make sure all tests pass.
453
+
454
+
455
+ ## Heroku Support
456
+
457
+ Unfortunately, as for now, Neo4j add-on on Heroku doesn't support Gremlin. Therefore, this gem won't work on Heroku's add on. You should self-host a Neo4j instance on an EC2 or any other server.
458
+
459
+
460
+ ## TO DO
461
+
462
+ [TO DO](https://github.com/elado/lexster/blob/master/TODO.md)
463
+
464
+
465
+ ---
466
+
467
+ Developed by [@elado](http://twitter.com/elado)