lexster 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)