ladder 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +190 -81
- data/ladder.gemspec +2 -2
- data/lib/ladder/file.rb +7 -2
- data/lib/ladder/searchable.rb +6 -75
- data/lib/ladder/searchable/file.rb +30 -0
- data/lib/ladder/searchable/resource.rb +78 -0
- data/lib/ladder/version.rb +1 -1
- data/spec/ladder/file/searchable_spec.rb +68 -0
- data/spec/ladder/{searchable_spec.rb → resource/searchable_spec.rb} +1 -1
- data/spec/shared/file.rb +1 -1
- metadata +10 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9239651dbb526c85d77203a89abcaf134ad353a4
|
4
|
+
data.tar.gz: e6c5b9a8bf16d50899a9d1ba9b5457d4bbd6c47b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d05d9401849734c2292ad8a4d0e1caf252132ee75b76c5a8941c440e0c54297097a75bf6bdbee5f5509fad19eb0ba511ad4284bfb5d55b4ac190b08033898d5d
|
7
|
+
data.tar.gz: 2fa63d70f71f806d83212032b1acde913963ccdf07e4336ee05ab6a0d0b86aea45e2f1866dbc226a2ae3081cd04a3dc88d68def4952a559d1d64527ec77ad074
|
data/README.md
CHANGED
@@ -41,13 +41,13 @@ Or install it yourself as:
|
|
41
41
|
* [Resources](#resources)
|
42
42
|
* [Configuring Resources](#configuring-resources)
|
43
43
|
* [Dynamic Resources](#dynamic-resources)
|
44
|
-
* [Files](#files)
|
45
44
|
* [Indexing for Search](#indexing-for-search)
|
45
|
+
* [Files](#files)
|
46
|
+
* [Indexing Files](#indexing-files)
|
46
47
|
|
47
48
|
### Resources
|
48
49
|
|
49
|
-
Much like ActiveTriples, Resources are the core of Ladder. Resources implement all the functionality of a Mongoid::Document and an ActiveTriples::Resource. To add Ladder integration for your model, require
|
50
|
-
and include the main module in your class:
|
50
|
+
Much like ActiveTriples, Resources are the core of Ladder. Resources implement all the functionality of a Mongoid::Document and an ActiveTriples::Resource. To add Ladder integration for your model, require and include the main module in your class:
|
51
51
|
|
52
52
|
```ruby
|
53
53
|
require 'ladder'
|
@@ -61,12 +61,13 @@ class Person
|
|
61
61
|
property :description, predicate: RDF::DC.description
|
62
62
|
end
|
63
63
|
|
64
|
-
steve = Person.new
|
65
|
-
|
66
|
-
|
67
|
-
|
64
|
+
steve = Person.new(first_name: 'Steve', description: 'Funny-looking')
|
65
|
+
=> #<Person _id: 542f0c124169720ea0000000, first_name: {"en"=>"Steve"}, description: {"en"=>"Funny-looking"}>
|
66
|
+
|
68
67
|
steve.as_document
|
69
|
-
|
68
|
+
=> {"_id"=>BSON::ObjectId('542f0c124169720ea0000000'),
|
69
|
+
"first_name"=>{"en"=>"Steve"},
|
70
|
+
"description"=>{"en"=>"Funny-looking"}}
|
70
71
|
|
71
72
|
steve.as_jsonld
|
72
73
|
# => {
|
@@ -249,8 +250,6 @@ Person.resource_class.base_uri
|
|
249
250
|
=> #<RDF::URI:0x3fecf69da274 URI:http://example.org/people>
|
250
251
|
|
251
252
|
Person.configure base_uri: 'http://some.other.uri/'
|
252
|
-
|
253
|
-
Person.resource_class.base_uri
|
254
253
|
=> "http://some.other.uri/"
|
255
254
|
```
|
256
255
|
|
@@ -268,12 +267,16 @@ class Person
|
|
268
267
|
end
|
269
268
|
|
270
269
|
steve = Person.new(first_name: 'Steve')
|
270
|
+
=> #<Person _id: 546669234169720397000000, first_name: {"en"=>"Steve"}>
|
271
271
|
|
272
272
|
steve.description
|
273
273
|
=> NoMethodError: undefined method 'description' for #<Person:0x007fb54eb1d0b8>
|
274
274
|
|
275
275
|
steve.property :description, predicate: RDF::DC.description
|
276
|
+
=> {:description=>"http://purl.org/dc/terms/description"}
|
277
|
+
|
276
278
|
steve.description = 'Funny-looking'
|
279
|
+
=> "Funny-looking"
|
277
280
|
|
278
281
|
steve.as_document
|
279
282
|
=> {"_id"=>BSON::ObjectId('546669234169720397000000'),
|
@@ -340,73 +343,6 @@ steve.as_jsonld
|
|
340
343
|
|
341
344
|
Note that due to the way Mongoid handles dynamic fields, dynamic properties **can not** be localized. They can be any kind of literal, but they **can not** be a related object. They can, however, contain a reference to the related object's URI.
|
342
345
|
|
343
|
-
### Files
|
344
|
-
|
345
|
-
Files are bytestreams that store binary content using MongoDB's GridFS storage system. They are still identifiable by a URI, and contain technical metadata about the File's contents.
|
346
|
-
|
347
|
-
```ruby
|
348
|
-
class Person
|
349
|
-
include Ladder::Resource
|
350
|
-
|
351
|
-
configure type: RDF::FOAF.Person
|
352
|
-
|
353
|
-
property :first_name, predicate: RDF::FOAF.name
|
354
|
-
property :thumbnails, predicate: RDF::FOAF.depiction, class_name: 'Image', inverse_of: nil
|
355
|
-
end
|
356
|
-
|
357
|
-
class Image
|
358
|
-
include Ladder::File
|
359
|
-
end
|
360
|
-
```
|
361
|
-
|
362
|
-
Similar to Resources, using `#property` as above will create a has-many relation for a File by default; however, because Files must be the target of a one-way relation, the `inverse_of: nil` option is required. Note that due to the way GridFS is designed, Files **can not** be embedded.
|
363
|
-
|
364
|
-
```ruby
|
365
|
-
steve = Person.new(first_name: 'Steve')
|
366
|
-
thumb = Image.new(file: open('http://some.image/pic.jpg'))
|
367
|
-
steve.thumbnails << thumb
|
368
|
-
|
369
|
-
steve.as_jsonld
|
370
|
-
# => {
|
371
|
-
# "@context": {
|
372
|
-
# "foaf": "http://xmlns.com/foaf/0.1/"
|
373
|
-
# },
|
374
|
-
# "@id": "http://example.org/people/549d83c64169720b32010000",
|
375
|
-
# "@type": "foaf:Person",
|
376
|
-
# "foaf:depiction": {
|
377
|
-
# "@id": "http://example.org/images/549d83c24169720b32000000"
|
378
|
-
# },
|
379
|
-
# "foaf:name": {
|
380
|
-
# "@language": "en",
|
381
|
-
# "@value": "Steve"
|
382
|
-
# }
|
383
|
-
# }
|
384
|
-
|
385
|
-
steve.save
|
386
|
-
# ... File is stored to GridFS ...
|
387
|
-
=> true
|
388
|
-
```
|
389
|
-
|
390
|
-
Files have all the attributes of a GridFS file, and the stored binary content is accessed using `#data`.
|
391
|
-
|
392
|
-
```ruby
|
393
|
-
thumb.reload
|
394
|
-
thumb.as_document
|
395
|
-
=> {"_id"=>BSON::ObjectId('549d86184169720b6a000000'),
|
396
|
-
"length"=>59709,
|
397
|
-
"chunkSize"=>4194304,
|
398
|
-
"uploadDate"=>2014-12-26 16:00:29 UTC,
|
399
|
-
"md5"=>"0d4a486e2cd71c51b7a92cfe96f29324",
|
400
|
-
"contentType"=>"image/jpeg",
|
401
|
-
"filename"=>"549d86184169720b6a000000/open-uri20141226-2922-u66ap6"}
|
402
|
-
|
403
|
-
thumb.length
|
404
|
-
=> 59709
|
405
|
-
|
406
|
-
thumb.data
|
407
|
-
=> # ... binary data ...
|
408
|
-
```
|
409
|
-
|
410
346
|
### Indexing for Search
|
411
347
|
|
412
348
|
You can also index your model classes for keyword searching through ElasticSearch by mixing in the Ladder::Searchable module:
|
@@ -422,9 +358,8 @@ class Person
|
|
422
358
|
property :description, predicate: RDF::DC.description
|
423
359
|
end
|
424
360
|
|
425
|
-
kimchy = Person.new
|
426
|
-
|
427
|
-
kimchy.description = 'Real genius'
|
361
|
+
kimchy = Person.new(first_name: 'Shay', description: 'Real genius')
|
362
|
+
=> #<Person _id: 543b457b41697231c5000000, first_name: {"en"=>"Shay"}, description: {"en"=>"Real genius"}>
|
428
363
|
```
|
429
364
|
|
430
365
|
In order to enable indexing, call the `#index_for_search` method on the class:
|
@@ -516,10 +451,14 @@ end
|
|
516
451
|
Person.property :projects, predicate: RDF::FOAF.made, class_name: 'Project'
|
517
452
|
|
518
453
|
es = Project.new(project_name: 'ElasticSearch', description: 'You know, for search')
|
454
|
+
=> #<Project _id: 544562c24169728b4e010000, project_name: {"en"=>"ElasticSearch"}, description: {"en"=>"You know, for search"}, developer_ids: nil>
|
455
|
+
|
519
456
|
es.developers << kimchy
|
457
|
+
=> [#<Person _id: 543b457b41697231c5000000, first_name: {"en"=>"Shay"}, description: {"en"=>"Real genius"}, project_ids: [BSON::ObjectId('544562c24169728b4e010000')]>]
|
520
458
|
|
521
459
|
Person.index_for_search as: :jsonld, related: true
|
522
460
|
=> :as_indexed_json
|
461
|
+
|
523
462
|
Project.index_for_search as: :jsonld, related: true
|
524
463
|
=> :as_indexed_json
|
525
464
|
|
@@ -593,6 +532,7 @@ es.as_indexed_json
|
|
593
532
|
|
594
533
|
Person.index_for_search as: :qname, related: true
|
595
534
|
=> :as_indexed_json
|
535
|
+
|
596
536
|
Project.index_for_search as: :qname, related: true
|
597
537
|
=> :as_indexed_json
|
598
538
|
|
@@ -647,6 +587,175 @@ es.as_indexed_json
|
|
647
587
|
# }
|
648
588
|
```
|
649
589
|
|
590
|
+
### Files
|
591
|
+
|
592
|
+
Files are bytestreams that store binary content using MongoDB's GridFS storage system. They are still identifiable by a URI, and contain technical metadata about the File's contents.
|
593
|
+
|
594
|
+
```ruby
|
595
|
+
class Person
|
596
|
+
include Ladder::Resource
|
597
|
+
|
598
|
+
configure type: RDF::FOAF.Person
|
599
|
+
|
600
|
+
property :first_name, predicate: RDF::FOAF.name
|
601
|
+
property :thumbnails, predicate: RDF::FOAF.depiction, class_name: 'Image', inverse_of: nil
|
602
|
+
end
|
603
|
+
|
604
|
+
class Image
|
605
|
+
include Ladder::File
|
606
|
+
end
|
607
|
+
```
|
608
|
+
|
609
|
+
Similar to Resources, using `#property` as above will create a has-many relation for a File by default; however, because Files must be the target of a one-way relation, the `inverse_of: nil` option is required. Note that due to the way GridFS is designed, Files **can not** be embedded.
|
610
|
+
|
611
|
+
```ruby
|
612
|
+
steve = Person.new(first_name: 'Steve')
|
613
|
+
=> #<Person _id: 549d83c64169720b32010000, first_name: {"en"=>"Steve"}>
|
614
|
+
|
615
|
+
thumb = Image.new(file: open('http://some.image/pic.jpg'))
|
616
|
+
=> #<Image _id: 549d83c24169720b32000000>
|
617
|
+
|
618
|
+
steve.thumbnails << thumb
|
619
|
+
=> [#<Image _id: 549d83c24169720b32000000, >]
|
620
|
+
|
621
|
+
steve.as_jsonld
|
622
|
+
# => {
|
623
|
+
# "@context": {
|
624
|
+
# "foaf": "http://xmlns.com/foaf/0.1/"
|
625
|
+
# },
|
626
|
+
# "@id": "http://example.org/people/549d83c64169720b32010000",
|
627
|
+
# "@type": "foaf:Person",
|
628
|
+
# "foaf:depiction": {
|
629
|
+
# "@id": "http://example.org/images/549d83c24169720b32000000"
|
630
|
+
# },
|
631
|
+
# "foaf:name": {
|
632
|
+
# "@language": "en",
|
633
|
+
# "@value": "Steve"
|
634
|
+
# }
|
635
|
+
# }
|
636
|
+
|
637
|
+
steve.save
|
638
|
+
# ... File is stored to GridFS ...
|
639
|
+
=> true
|
640
|
+
```
|
641
|
+
|
642
|
+
Files have all the attributes of a GridFS file, and the stored binary content is accessed using `#data`.
|
643
|
+
|
644
|
+
```ruby
|
645
|
+
thumb.reload
|
646
|
+
=> #<Image _id: 549d86184169720b6a000000, >
|
647
|
+
|
648
|
+
thumb.as_document
|
649
|
+
=> {"_id"=>BSON::ObjectId('549d86184169720b6a000000'),
|
650
|
+
"length"=>59709,
|
651
|
+
"chunkSize"=>4194304,
|
652
|
+
"uploadDate"=>2014-12-26 16:00:29 UTC,
|
653
|
+
"md5"=>"0d4a486e2cd71c51b7a92cfe96f29324",
|
654
|
+
"contentType"=>"image/jpeg",
|
655
|
+
"filename"=>"549d86184169720b6a000000/open-uri20141226-2922-u66ap6"}
|
656
|
+
|
657
|
+
thumb.length
|
658
|
+
=> 59709
|
659
|
+
|
660
|
+
thumb.data
|
661
|
+
=> # ... binary data ...
|
662
|
+
```
|
663
|
+
|
664
|
+
#### Indexing Files
|
665
|
+
|
666
|
+
Files that contain textual content (eg. HTML, PDF, ePub, DOC, etc) can be automatically indexed when they are persisted, again just by mixing in the Ladder::Searchable module (there is no need to call `#index_for_search` on the class). Note that this requires the [Mapper Attachments Plugin for Elasticsearch](https://github.com/elasticsearch/elasticsearch-mapper-attachments) to be installed.
|
667
|
+
|
668
|
+
```ruby
|
669
|
+
class OCR
|
670
|
+
include Ladder::File
|
671
|
+
include Ladder::Searchable
|
672
|
+
end
|
673
|
+
|
674
|
+
pdf = OCR.new(file: open('http://some.location/ocr.pdf'))
|
675
|
+
=> #<OCR _id: 54add77a4169721c23000000>
|
676
|
+
|
677
|
+
pdf.save
|
678
|
+
=> true
|
679
|
+
|
680
|
+
results = OCR.search 'Moomintroll'
|
681
|
+
# => #<Elasticsearch::Model::Response::Response:0x007fa2ca82a9f0
|
682
|
+
# @klass=[PROXY] OCR,
|
683
|
+
# @search=
|
684
|
+
# #<Elasticsearch::Model::Searching::SearchRequest:0x007fa2ca830a58
|
685
|
+
# @definition={:index=>"ocrs", :type=>"ocr", :q=>"Moomintroll"},
|
686
|
+
# @klass=[PROXY] OCR,
|
687
|
+
# @options={}>>
|
688
|
+
|
689
|
+
results.count
|
690
|
+
=> 1
|
691
|
+
|
692
|
+
results.records.first == pdf
|
693
|
+
=> true
|
694
|
+
|
695
|
+
results.records.first.as_document
|
696
|
+
=> {"_id"=>BSON::ObjectId('54add77a4169721c23000000'),
|
697
|
+
"length"=>12941,
|
698
|
+
"chunkSize"=>4194304,
|
699
|
+
"uploadDate"=>2015-01-08 01:03:54 UTC,
|
700
|
+
"md5"=>"831a47b953d6e11d17cee7de9abd73c4",
|
701
|
+
"contentType"=>"application/pdf",
|
702
|
+
"filename"=>"54add77a4169721c23000000/ocr.pdf"}
|
703
|
+
|
704
|
+
results.records.first.data
|
705
|
+
=> # ... binary data ...
|
706
|
+
```
|
707
|
+
|
708
|
+
This can be useful if you want to retrieve a File by searching for the textual content that it contains. Note the use of `#records` to access the Ladder::File instances directly ([see here for more information](https://github.com/elasticsearch/elasticsearch-rails/tree/master/elasticsearch-model#search-results-as-database-records)). However, if you want to get information about the file characteristics (including the extracted textual content), you can use a modified search query:
|
709
|
+
|
710
|
+
```ruby
|
711
|
+
results = OCR.search 'Moomintroll', fields: '*'
|
712
|
+
# => #<Elasticsearch::Model::Response::Response:0x007fc36cadaa20
|
713
|
+
# @klass=[PROXY] OCR,
|
714
|
+
# @search=
|
715
|
+
# #<Elasticsearch::Model::Searching::SearchRequest:0x007fc36cadab10
|
716
|
+
# @definition={:index=>"ocrs", :type=>"ocr", :body=>{:query=>{:query_string=>{:query=>"Moomintroll"}}, :fields=>"*"}},
|
717
|
+
# @klass=[PROXY] OCR,
|
718
|
+
# @options={}>>
|
719
|
+
|
720
|
+
results.count
|
721
|
+
=> 1
|
722
|
+
|
723
|
+
results.first.fields
|
724
|
+
=> {
|
725
|
+
"file.content_type"=>["application/pdf"],
|
726
|
+
"file.keywords"=>[""],
|
727
|
+
"file"=>
|
728
|
+
["\nAnd so Moomintroll was helplessly thrown out into a strange and dangerous world and \ndropped up to his ears in the first snowdrift of his experience. It felt unpleasantly prickly \nto his velvet skin, but at the same time his nose caught a new smell. It was a more \nserious smell than any he had met before, and slightly frightening. But it made him wide \nawake and greatly interested.\n\n\n"],
|
729
|
+
"file.date"=>["2014-12-19T15:32:58Z"],
|
730
|
+
"file.title"=>["Untitled"]}
|
731
|
+
```
|
732
|
+
|
733
|
+
In this case, the `#fields` Hash contains all of the technical metadata obtained by Elasticsearch during indexing. Note that this is **not the same** as the metadata stored by GridFS (with the possible exception of content type). Finally, we can also provide contextual highlighting for search results by using a slightly more complex search query:
|
734
|
+
|
735
|
+
```ruby
|
736
|
+
results = OCR.search query: { query_string: { query: 'his' } }, highlight: { fields: { file: {} } }
|
737
|
+
# => #<Elasticsearch::Model::Response::Response:0x007fd653dc8b48
|
738
|
+
# @klass=[PROXY] OCR,
|
739
|
+
# @search=
|
740
|
+
# #<Elasticsearch::Model::Searching::SearchRequest:0x007fd653dc8b48
|
741
|
+
# @definition={:index=>"ocrs", :type=>"ocr", :body=>{:query=>{:query_string=>"Moomintroll"},
|
742
|
+
# :highlight=>{:fields=>{:file=>{}}}}},
|
743
|
+
# @klass=[PROXY] OCR,
|
744
|
+
# @options={}>>
|
745
|
+
|
746
|
+
results.count
|
747
|
+
=> 1
|
748
|
+
|
749
|
+
results.first.highlight.file.count
|
750
|
+
=> 2
|
751
|
+
|
752
|
+
results.first.highlight.file
|
753
|
+
=> [" <em>his</em> ears in the first snowdrift of <em>his</em> experience. It felt unpleasantly prickly \nto <em>his</em> velvet skin",
|
754
|
+
", but at the same time <em>his</em> nose caught a new smell. It was a more \nserious smell than any he had met"]
|
755
|
+
```
|
756
|
+
|
757
|
+
More information about performing highlighting queries is available in the [Elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-highlighting.html).
|
758
|
+
|
650
759
|
## Contributing
|
651
760
|
|
652
761
|
Anyone and everyone is welcome to contribute. Go crazy.
|
@@ -670,4 +779,4 @@ Many thanks to Christopher Knight [@NomadicKnight](https://twitter.com/Nomadic_K
|
|
670
779
|
## License
|
671
780
|
|
672
781
|
Apache License Version 2.0
|
673
|
-
http://apache.org/licenses/LICENSE-2.0.txt
|
782
|
+
http://apache.org/licenses/LICENSE-2.0.txt
|
data/ladder.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.platform = Gem::Platform::RUBY
|
10
10
|
spec.authors = "MJ Suhonos"
|
11
11
|
spec.email = "mj@suhonos.ca"
|
12
|
-
spec.summary = %q{
|
13
|
-
spec.description = %q{
|
12
|
+
spec.summary = %q{ActiveModel Linked Data framework.}
|
13
|
+
spec.description = %q{Dynamic framework for Linked Data modelling, persistence, and full-text indexing.}
|
14
14
|
spec.homepage = "https://github.com/ladder/ladder"
|
15
15
|
spec.license = "APACHE2"
|
16
16
|
spec.required_ruby_version = '>= 1.9.3'
|
data/lib/ladder/file.rb
CHANGED
@@ -25,10 +25,15 @@ module Ladder::File
|
|
25
25
|
##
|
26
26
|
# Make save behave like Mongoid::Document as much as possible
|
27
27
|
def save
|
28
|
+
# FIXME: this raises on a freshly-retrieved document; should either set @file or catch @grid_file
|
28
29
|
raise Mongoid::Errors::InvalidValue.new(IO, NilClass) if file.nil?
|
29
30
|
|
30
|
-
|
31
|
-
|
31
|
+
persisted? ? run_callbacks(:update) : run_callbacks(:create)
|
32
|
+
|
33
|
+
run_callbacks(:save) do
|
34
|
+
attributes[:content_type] = file.content_type if file.respond_to? :content_type
|
35
|
+
@grid_file ? @grid_file.save : !! @grid_file = self.class.grid.put(file, attributes.symbolize_keys)
|
36
|
+
end
|
32
37
|
end
|
33
38
|
|
34
39
|
##
|
data/lib/ladder/searchable.rb
CHANGED
@@ -5,83 +5,14 @@ require 'elasticsearch/model/callbacks'
|
|
5
5
|
module Ladder::Searchable
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
+
autoload :Resource, 'ladder/searchable/resource'
|
9
|
+
autoload :File, 'ladder/searchable/file'
|
10
|
+
|
8
11
|
included do
|
9
12
|
include Elasticsearch::Model
|
10
13
|
include Elasticsearch::Model::Callbacks
|
11
|
-
end
|
12
|
-
|
13
|
-
##
|
14
|
-
# Generate a qname-based JSON representation
|
15
|
-
#
|
16
|
-
def as_qname(opts = {})
|
17
|
-
qname_hash = type.empty? ? {} : {rdf: {type: type.first.pname }}
|
18
|
-
|
19
|
-
resource_class.properties.each do |field_name, property|
|
20
|
-
ns, name = property.predicate.qname
|
21
|
-
qname_hash[ns] ||= Hash.new
|
22
|
-
|
23
|
-
object = self.send(field_name)
|
24
|
-
|
25
|
-
if relations.keys.include? field_name
|
26
|
-
if opts[:related]
|
27
|
-
qname_hash[ns][name] = object.to_a.map { |obj| obj.as_qname }
|
28
|
-
else
|
29
|
-
qname_hash[ns][name] = object.to_a.map { |obj| "#{obj.class.name.underscore.pluralize}:#{obj.id}" }
|
30
|
-
end
|
31
|
-
elsif fields.keys.include? field_name
|
32
|
-
qname_hash[ns][name] = read_attribute(field_name)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
qname_hash
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
##
|
42
|
-
# Return a framed, compacted JSON-LD representation
|
43
|
-
# by embedding related objects from the graph
|
44
|
-
#
|
45
|
-
# NB: Will NOT embed related objects with same @type. Spec under discussion, see https://github.com/json-ld/json-ld.org/issues/110
|
46
|
-
def as_framed_jsonld
|
47
|
-
json_hash = as_jsonld related: true
|
48
|
-
context = json_hash['@context']
|
49
|
-
frame = {'@context' => context, '@type' => type.first.pname}
|
50
|
-
JSON::LD::API.compact(JSON::LD::API.frame(json_hash, frame), context)
|
51
|
-
end
|
52
|
-
|
53
|
-
##
|
54
|
-
# Force autosave of related documents using Mongoid-defined methods
|
55
|
-
# Required for explicit autosave prior to after_update index callbacks
|
56
|
-
#
|
57
|
-
def autosave
|
58
|
-
methods.select{|i| i[/autosave_documents/] }.each{|m| send m}
|
59
|
-
end
|
60
|
-
|
61
|
-
module ClassMethods
|
62
|
-
|
63
|
-
##
|
64
|
-
# Specify type of serialization to use for indexing
|
65
|
-
#
|
66
|
-
def index_for_search(opts = {})
|
67
|
-
case opts[:as]
|
68
|
-
when :jsonld
|
69
|
-
if opts[:related]
|
70
|
-
define_method(:as_indexed_json) { |opts = {}| autosave; as_framed_jsonld }
|
71
|
-
else
|
72
|
-
define_method(:as_indexed_json) { |opts = {}| as_jsonld }
|
73
|
-
end
|
74
|
-
when :qname
|
75
|
-
if opts[:related]
|
76
|
-
define_method(:as_indexed_json) { |opts = {}| as_qname related: true }
|
77
|
-
else
|
78
|
-
define_method(:as_indexed_json) { |opts = {}| as_qname }
|
79
|
-
end
|
80
|
-
else
|
81
|
-
define_method(:as_indexed_json) { |opts = {}| as_json(except: ['id', '_id']) }
|
82
|
-
end
|
83
|
-
end
|
84
14
|
|
85
|
-
|
86
|
-
|
15
|
+
include Ladder::Searchable::Resource if self.ancestors.include? Ladder::Resource
|
16
|
+
include Ladder::Searchable::File if self.ancestors.include? Ladder::File
|
17
|
+
end
|
87
18
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ladder::Searchable::File
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
# Index binary content using Elasticsearch mapper attachment plugin
|
6
|
+
# https://github.com/elasticsearch/elasticsearch-mapper-attachments
|
7
|
+
mapping _source: { enabled: false } do
|
8
|
+
indexes :file, type: 'attachment', fields: {
|
9
|
+
file: { store: true },
|
10
|
+
title: { store: true },
|
11
|
+
date: { store: true },
|
12
|
+
author: { store: true },
|
13
|
+
keywords: { store: true },
|
14
|
+
content_type: { store: true },
|
15
|
+
content_length: { store: true },
|
16
|
+
language: { store: true }
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Explicitly set mapping definition on index
|
21
|
+
self.__elasticsearch__.create_index!
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Return a Base64-encoded copy of data
|
26
|
+
def as_indexed_json(opts = {})
|
27
|
+
{ file: Base64.encode64(data) }
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Ladder::Searchable::Resource
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
##
|
5
|
+
# Generate a qname-based JSON representation
|
6
|
+
#
|
7
|
+
def as_qname(opts = {})
|
8
|
+
qname_hash = type.empty? ? {} : {rdf: {type: type.first.pname }}
|
9
|
+
|
10
|
+
resource_class.properties.each do |field_name, property|
|
11
|
+
ns, name = property.predicate.qname
|
12
|
+
qname_hash[ns] ||= Hash.new
|
13
|
+
|
14
|
+
object = self.send(field_name)
|
15
|
+
|
16
|
+
if relations.keys.include? field_name
|
17
|
+
if opts[:related]
|
18
|
+
qname_hash[ns][name] = object.to_a.map { |obj| obj.as_qname }
|
19
|
+
else
|
20
|
+
qname_hash[ns][name] = object.to_a.map { |obj| "#{obj.class.name.underscore.pluralize}:#{obj.id}" }
|
21
|
+
end
|
22
|
+
elsif fields.keys.include? field_name
|
23
|
+
qname_hash[ns][name] = read_attribute(field_name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
qname_hash
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
##
|
33
|
+
# Return a framed, compacted JSON-LD representation
|
34
|
+
# by embedding related objects from the graph
|
35
|
+
#
|
36
|
+
# NB: Will NOT embed related objects with same @type. Spec under discussion, see https://github.com/json-ld/json-ld.org/issues/110
|
37
|
+
def as_framed_jsonld
|
38
|
+
json_hash = as_jsonld related: true
|
39
|
+
context = json_hash['@context']
|
40
|
+
frame = {'@context' => context, '@type' => type.first.pname}
|
41
|
+
JSON::LD::API.compact(JSON::LD::API.frame(json_hash, frame), context)
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Force autosave of related documents using Mongoid-defined methods
|
46
|
+
# Required for explicit autosave prior to after_update index callbacks
|
47
|
+
#
|
48
|
+
def autosave
|
49
|
+
methods.select{|i| i[/autosave_documents/] }.each{|m| send m}
|
50
|
+
end
|
51
|
+
|
52
|
+
module ClassMethods
|
53
|
+
|
54
|
+
##
|
55
|
+
# Specify type of serialization to use for indexing
|
56
|
+
#
|
57
|
+
def index_for_search(opts = {})
|
58
|
+
case opts[:as]
|
59
|
+
when :jsonld
|
60
|
+
if opts[:related]
|
61
|
+
define_method(:as_indexed_json) { |opts = {}| autosave; as_framed_jsonld }
|
62
|
+
else
|
63
|
+
define_method(:as_indexed_json) { |opts = {}| as_jsonld }
|
64
|
+
end
|
65
|
+
when :qname
|
66
|
+
if opts[:related]
|
67
|
+
define_method(:as_indexed_json) { |opts = {}| as_qname related: true }
|
68
|
+
else
|
69
|
+
define_method(:as_indexed_json) { |opts = {}| as_qname }
|
70
|
+
end
|
71
|
+
else
|
72
|
+
define_method(:as_indexed_json) { |opts = {}| as_json(except: [:id, :_id]) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
data/lib/ladder/version.rb
CHANGED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ladder::Searchable::File do
|
4
|
+
before do
|
5
|
+
Mongoid.load!('mongoid.yml', :development)
|
6
|
+
Mongoid.logger.level = Moped.logger.level = Logger::DEBUG
|
7
|
+
Mongoid.purge!
|
8
|
+
|
9
|
+
Elasticsearch::Model.client = Elasticsearch::Client.new host: 'localhost:9200', log: true
|
10
|
+
Elasticsearch::Model.client.indices.delete index: '_all'
|
11
|
+
|
12
|
+
LADDER_BASE_URI = 'http://example.org'
|
13
|
+
|
14
|
+
class Datastream
|
15
|
+
include Ladder::File
|
16
|
+
include Ladder::Searchable
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
shared_context 'searchable' do
|
21
|
+
before do
|
22
|
+
subject.save
|
23
|
+
Elasticsearch::Model.client.indices.flush
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should exist in the index' do
|
27
|
+
results = subject.class.search('Moomin*')
|
28
|
+
expect(results.count).to eq 1
|
29
|
+
expect(results.first.id).to eq subject.id.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should contain full-text content' do
|
33
|
+
results = subject.class.search 'Moomin*', fields: '*'
|
34
|
+
expect(results.count).to eq 1
|
35
|
+
expect(results.first.fields.file.first).to include 'Moomin'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'with data from file' do
|
40
|
+
TEST_FILE = './spec/shared/moomin.pdf'
|
41
|
+
|
42
|
+
let(:subject) { Datastream.new file: open(TEST_FILE) }
|
43
|
+
let(:source) { open(TEST_FILE).read } # ASCII-8BIT (binary)
|
44
|
+
|
45
|
+
it_behaves_like 'a File'
|
46
|
+
|
47
|
+
include_context 'searchable'
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'with data from string after creation' do
|
51
|
+
data = "And so Moomintroll was helplessly thrown out into a strange and dangerous world and dropped up to his ears in the first snowdrift of his experience. It felt unpleasantly prickly to his velvet skin, but at the same time his nose caught a new smell. It was a more serious smell than any he had met before, and slightly frightening. But it made him wide awake and greatly interested."
|
52
|
+
|
53
|
+
let(:subject) { Datastream.new }
|
54
|
+
let(:source) { data } # UTF-8 (string)
|
55
|
+
|
56
|
+
before do
|
57
|
+
subject.file = StringIO.new(source)
|
58
|
+
end
|
59
|
+
|
60
|
+
it_behaves_like 'a File'
|
61
|
+
|
62
|
+
include_context 'searchable'
|
63
|
+
end
|
64
|
+
|
65
|
+
after do
|
66
|
+
Object.send(:remove_const, "Datastream") if Object
|
67
|
+
end
|
68
|
+
end
|
data/spec/shared/file.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ladder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- MJ Suhonos
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-01-
|
11
|
+
date: 2015-01-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mongoid
|
@@ -178,7 +178,7 @@ dependencies:
|
|
178
178
|
- - "~>"
|
179
179
|
- !ruby/object:Gem::Version
|
180
180
|
version: '0.9'
|
181
|
-
description:
|
181
|
+
description: Dynamic framework for Linked Data modelling, persistence, and full-text
|
182
182
|
indexing.
|
183
183
|
email: mj@suhonos.ca
|
184
184
|
executables: []
|
@@ -199,13 +199,16 @@ files:
|
|
199
199
|
- lib/ladder/resource.rb
|
200
200
|
- lib/ladder/resource/dynamic.rb
|
201
201
|
- lib/ladder/searchable.rb
|
202
|
+
- lib/ladder/searchable/file.rb
|
203
|
+
- lib/ladder/searchable/resource.rb
|
202
204
|
- lib/ladder/version.rb
|
203
205
|
- logo.png
|
204
206
|
- mongoid.yml
|
207
|
+
- spec/ladder/file/searchable_spec.rb
|
205
208
|
- spec/ladder/file_spec.rb
|
206
209
|
- spec/ladder/resource/dynamic_spec.rb
|
210
|
+
- spec/ladder/resource/searchable_spec.rb
|
207
211
|
- spec/ladder/resource_spec.rb
|
208
|
-
- spec/ladder/searchable_spec.rb
|
209
212
|
- spec/shared/file.rb
|
210
213
|
- spec/shared/moomin.pdf
|
211
214
|
- spec/shared/resource.rb
|
@@ -233,12 +236,13 @@ rubyforge_project:
|
|
233
236
|
rubygems_version: 2.2.2
|
234
237
|
signing_key:
|
235
238
|
specification_version: 4
|
236
|
-
summary:
|
239
|
+
summary: ActiveModel Linked Data framework.
|
237
240
|
test_files:
|
241
|
+
- spec/ladder/file/searchable_spec.rb
|
238
242
|
- spec/ladder/file_spec.rb
|
239
243
|
- spec/ladder/resource/dynamic_spec.rb
|
244
|
+
- spec/ladder/resource/searchable_spec.rb
|
240
245
|
- spec/ladder/resource_spec.rb
|
241
|
-
- spec/ladder/searchable_spec.rb
|
242
246
|
- spec/shared/file.rb
|
243
247
|
- spec/shared/moomin.pdf
|
244
248
|
- spec/shared/resource.rb
|