rbbt-text 0.2.1 → 0.5.0

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.
Files changed (57) hide show
  1. data/bin/get_ppis.rb +52 -0
  2. data/lib/rbbt/bow/dictionary.rb +9 -9
  3. data/lib/rbbt/bow/misc.rb +86 -2
  4. data/lib/rbbt/corpus/corpus.rb +55 -0
  5. data/lib/rbbt/corpus/document.rb +289 -0
  6. data/lib/rbbt/corpus/document_repo.rb +115 -0
  7. data/lib/rbbt/corpus/sources/pubmed.rb +26 -0
  8. data/lib/rbbt/ner/NER.rb +7 -5
  9. data/lib/rbbt/ner/abner.rb +13 -2
  10. data/lib/rbbt/ner/annotations.rb +182 -51
  11. data/lib/rbbt/ner/annotations/annotated.rb +15 -0
  12. data/lib/rbbt/ner/annotations/named_entity.rb +37 -0
  13. data/lib/rbbt/ner/annotations/relations.rb +25 -0
  14. data/lib/rbbt/ner/annotations/token.rb +28 -0
  15. data/lib/rbbt/ner/annotations/transformed.rb +170 -0
  16. data/lib/rbbt/ner/banner.rb +8 -5
  17. data/lib/rbbt/ner/chemical_tagger.rb +34 -0
  18. data/lib/rbbt/ner/ngram_prefix_dictionary.rb +136 -0
  19. data/lib/rbbt/ner/oscar3.rb +1 -1
  20. data/lib/rbbt/ner/oscar4.rb +41 -0
  21. data/lib/rbbt/ner/patterns.rb +132 -0
  22. data/lib/rbbt/ner/rnorm.rb +141 -0
  23. data/lib/rbbt/ner/rnorm/cue_index.rb +80 -0
  24. data/lib/rbbt/ner/rnorm/tokens.rb +218 -0
  25. data/lib/rbbt/ner/token_trieNER.rb +185 -51
  26. data/lib/rbbt/nlp/genia/sentence_splitter.rb +214 -0
  27. data/lib/rbbt/nlp/nlp.rb +235 -0
  28. data/share/install/software/ABNER +0 -4
  29. data/share/install/software/ChemicalTagger +81 -0
  30. data/share/install/software/Gdep +115 -0
  31. data/share/install/software/Geniass +118 -0
  32. data/share/install/software/OSCAR4 +16 -0
  33. data/share/install/software/StanfordParser +15 -0
  34. data/share/patterns/drug_induce_disease +22 -0
  35. data/share/rnorm/cue_default +10 -0
  36. data/share/rnorm/tokens_default +86 -0
  37. data/share/{stopwords → wordlists/stopwords} +0 -0
  38. data/test/rbbt/bow/test_bow.rb +1 -1
  39. data/test/rbbt/bow/test_dictionary.rb +1 -1
  40. data/test/rbbt/bow/test_misc.rb +1 -1
  41. data/test/rbbt/corpus/test_corpus.rb +99 -0
  42. data/test/rbbt/corpus/test_document.rb +222 -0
  43. data/test/rbbt/ner/annotations/test_named_entity.rb +14 -0
  44. data/test/rbbt/ner/annotations/test_transformed.rb +175 -0
  45. data/test/rbbt/ner/test_abner.rb +1 -1
  46. data/test/rbbt/ner/test_annotations.rb +64 -2
  47. data/test/rbbt/ner/test_banner.rb +1 -1
  48. data/test/rbbt/ner/test_chemical_tagger.rb +56 -0
  49. data/test/rbbt/ner/test_ngram_prefix_dictionary.rb +20 -0
  50. data/test/rbbt/ner/{test_oscar3.rb → test_oscar4.rb} +12 -13
  51. data/test/rbbt/ner/test_patterns.rb +66 -0
  52. data/test/rbbt/ner/test_regexpNER.rb +1 -1
  53. data/test/rbbt/ner/test_rnorm.rb +47 -0
  54. data/test/rbbt/ner/test_token_trieNER.rb +60 -35
  55. data/test/rbbt/nlp/test_nlp.rb +88 -0
  56. data/test/test_helper.rb +20 -0
  57. metadata +93 -20
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rbbt-util'
4
+ require 'rbbt/annotations/corpus'
5
+ require 'rbbt/annotations/corpus/pubmed'
6
+ require 'rbbt/annotations/relationships/ppi'
7
+ require 'rbbt/sources/pubmed'
8
+ require 'rbbt/ner/annotations'
9
+ require 'rbbt/ner/token_trieNER'
10
+ require 'rbbt/ner/annotations/transformed'
11
+ require 'rbbt/ner/chemical_tagger'
12
+
13
+ Corpus.define_entity_ner "Compounds", false do |doc|
14
+ @@chemical_tagger ||= ChemicalTagger.new
15
+ @@chemical_tagger.entities(doc.text)
16
+ end
17
+
18
+ Corpus.define_entity_ner "Diseases", false do |doc|
19
+ if ! defined? @@tokenizer
20
+ @@tokenizer = TokenTrieNER.new [], :longest_match => true
21
+ @@tokenizer.merge TSV.new(Rbbt.share.databases.COSTART.COSTART, :native => 0, :extra => 0, :flatten => true), :COSTART
22
+ @@tokenizer.merge TSV.new(Rbbt.share.databases.CTCAE.CTCAE, :native => 0, :extra => 1, :flatten => true), :CTCAE
23
+ @@tokenizer.merge Rbbt.share.databases.Polysearch.disease, :disease
24
+ end
25
+ @@tokenizer.entities(doc.text)
26
+ end
27
+
28
+ corpus = Corpus.new Rbbt.tmp.corpus["PPIS2"].find
29
+
30
+ docids = corpus.add_pubmed_query("Cancer", 5000, :abstract)
31
+
32
+ Misc.profile do
33
+ docids[0..100].each do |docid|
34
+ puts "ARTICLE: #{ docid }"
35
+ doc = corpus.docid(docid)
36
+ diseases = doc.produce_diseases
37
+ #puts "Diseases: #{diseases.collect{|g| [g,g.id,g.offset] * ":"} * ", "}"
38
+ #sentences = doc.sentences
39
+ #diseases_index = Segment.index(diseases)
40
+ #sentences.each do |sentence|
41
+ # diseases_in_sentence = diseases_index[sentence.range]
42
+ # next if diseases_in_sentence.empty?
43
+ # Transformed.transform(sentence, sentence.make_relative(diseases_in_sentence.dup)) do |entity|
44
+ # entity.html
45
+ # end
46
+ # puts "---#{[sentence.id, sentence.offset] * ":"}"
47
+ # puts sentence
48
+ # puts "Diseases: #{diseases_in_sentence.collect{|g| [g,g.id,g.offset] * ":"} * ", "}"
49
+ # sentence.restore
50
+ #end
51
+ end
52
+ end
@@ -74,15 +74,15 @@ class Dictionary::TF_IDF
74
74
  end
75
75
 
76
76
  def best(options = {})
77
- hi, low, limit = {
77
+ high, low, limit = {
78
78
  :low => 0,
79
- :hi => 1,
79
+ :high => 1,
80
80
  }.merge(options).
81
- values_at(:hi, :low, :limit)
81
+ values_at(:high, :low, :limit)
82
82
 
83
83
  num_docs = @num_docs.to_f
84
84
  best = df.select{|term, value|
85
- value >= low && value <= hi
85
+ value >= low && value <= high
86
86
  }.collect{|p|
87
87
  term = p.first
88
88
  df_value = p.last
@@ -147,19 +147,19 @@ class Dictionary::KL
147
147
  end
148
148
 
149
149
  def best(options = {})
150
- hi, low, limit = {
150
+ high, low, limit = {
151
151
  :low => 0,
152
- :hi => 1,
152
+ :high => 1,
153
153
  }.merge(options).
154
- values_at(:hi, :low, :limit)
154
+ values_at(:high, :low, :limit)
155
155
 
156
156
  pos_df = @pos_dict.df
157
157
  neg_df = @neg_dict.df
158
158
 
159
159
  best = {}
160
160
  terms.select{|term|
161
- pos_df[term] >= low && pos_df[term] <= hi ||
162
- neg_df[term] >= low && neg_df[term] <= hi
161
+ pos_df[term] >= low && pos_df[term] <= high ||
162
+ neg_df[term] >= low && neg_df[term] <= high
163
163
  }.each{|term|
164
164
  pos = pos_df[term]
165
165
  neg = neg_df[term]
@@ -1,7 +1,91 @@
1
1
  require 'rbbt'
2
2
  require 'rbbt/util/open'
3
3
 
4
- Rbbt.claim 'stopwords', 'stopwords', 'wordlist'
4
+ Rbbt.share.wordlists.trigger_terms.define_as_url "http://zope.bioinfo.cnio.es/hpylori/pubmedxml2dir_files/ppi_trigger_term_table.txt"
5
5
 
6
- $stopwords = Rbbt.files.wordlists.stopwords.read.scan(/\w+/)
6
+ $stopwords = Rbbt.share.wordlists.stopwords.read.scan(/\w+/)
7
+
8
+ $greek = {
9
+ "alpha" => "a",
10
+ "beta" => "b",
11
+ "gamma" => "g",
12
+ "delta" => "d",
13
+ "epsilon" => "e",
14
+ "zeta" => "z",
15
+ "eta" => "e",
16
+ "theta" => "th",
17
+ "iota" => "i",
18
+ "kappa" => "k",
19
+ "lambda" => "l",
20
+ "mu" => "m",
21
+ "nu" => "n",
22
+ "xi" => "x",
23
+ "omicron" => "o",
24
+ "pi" => "p",
25
+ "rho" => "r",
26
+ "sigma" => "s",
27
+ "tau" => "t",
28
+ "upsilon" => "u",
29
+ "phi" => "ph",
30
+ "chi" => "ch",
31
+ "psi" => "ps",
32
+ "omega" => "o"
33
+ }
34
+
35
+ $inverse_greek = Hash.new
36
+ $greek.each{|l,s| $inverse_greek[s] = l }
37
+
38
+ class String
39
+ CONSONANTS = []
40
+ if File.exists? File.join(Rbbt.datadir, 'wordlists/consonants')
41
+ Object::Open.read(File.join(Rbbt.datadir, 'wordlists/consonants')).each_line{|l| CONSONANTS << l.chomp}
42
+ end
43
+
44
+ # Uses heuristics to checks if a string seems like a special word, like a gene name.
45
+ def is_special?
46
+ # Only consonants
47
+ return true if self =~ /^[bcdfghjklmnpqrstvwxz]+$/i
48
+
49
+ # Not a word
50
+ return false if self =~ /[^\s]\s[^\s]/;
51
+ return false if self.length < 3;
52
+ # Alphanumeric
53
+ return true if self =~ /[0-9]/ && self =~ /[a-z]/i
54
+ # All Caps
55
+ return true if self =~ /[A-Z]{2,}/;
56
+ # Caps Mix
57
+ return true if self =~ /[a-z][A-Z]/;
58
+ # All consonants
59
+ return true if self =~ /^[a-z]$/i && self !~ /[aeiou]/i
60
+ # Dashed word
61
+ return true if self =~ /(^\w-|-\w$)/
62
+ # To many consonants (very heuristic)
63
+ if self =~ /([^aeiouy]{3,})/i && !CONSONANTS.include?($1.downcase)
64
+ return true
65
+ end
66
+
67
+ return false
68
+ end
69
+
70
+ # Turns the first letter to lowercase
71
+ def downcase_first
72
+ return "" if self == ""
73
+ letters = self.scan(/./)
74
+ letters[0].downcase!
75
+ letters.join("")
76
+ end
77
+
78
+ # Turns a roman number into arabic form is possible. Just simple
79
+ # romans only...
80
+ def arabic
81
+ return 1 if self =~ /^I$/;
82
+ return 2 if self =~ /^II$/;
83
+ return 3 if self =~ /^III$/;
84
+ return 4 if self =~ /^IV$/;
85
+ return 5 if self =~ /^V$/;
86
+ return 10 if self =~ /^X$/;
87
+
88
+ return nil
89
+ end
90
+ end
7
91
 
@@ -0,0 +1,55 @@
1
+ require 'rbbt/corpus/document'
2
+ require 'rbbt/corpus/document_repo'
3
+
4
+ class Corpus
5
+ attr_accessor :corpora_path, :document_repo, :persistence_dir, :global_annotations
6
+ def initialize(corpora_path = nil)
7
+ @corpora_path = case
8
+ when corpora_path.nil?
9
+ Rbbt.corpora
10
+ when (not Resource::Path === corpora_path)
11
+ Resource::Path.path(corpora_path)
12
+ else
13
+ corpora_path
14
+ end
15
+
16
+ @document_repo = DocumentRepo.get @corpora_path.document_repo, false
17
+ @persistence_dir = File.join(@corpora_path, "annotations")
18
+ @global_annotations = TSV.new(TCHash.get(File.join(@persistence_dir, "global_annotations"), :list), :list, :key => "ID", :fields => [ "Start", "End", "Info","Document ID", "Entity Type"])
19
+ @global_annotations.unnamed = true
20
+ end
21
+
22
+ def persistence_for(docid)
23
+ File.join(persistence_dir, docid)
24
+ end
25
+
26
+ def document(namespace, id, type, hash)
27
+ docid = [namespace, id, type, hash] * ":"
28
+ Document.new(persistence_for(docid), docid, @document_repo[docid], @global_annotations)
29
+ end
30
+
31
+ def docid(docid)
32
+ Document.new(persistence_for(docid), docid, @document_repo[docid], @global_annotations)
33
+ end
34
+
35
+ def add_document(text, namespace, id, type = nil)
36
+ hash = Digest::MD5.hexdigest(text)
37
+ @document_repo.add(text, namespace, id, type, hash)
38
+ end
39
+
40
+ def find(namespace=nil, id = nil, type = nil, hash = nil)
41
+ @document_repo.find(namespace, id, type, hash).collect{|docid|
42
+ Document.new(persistence_for(docid), docid, @document_repo[docid], @global_annotations)
43
+ }
44
+ end
45
+
46
+ def find_docid(docid)
47
+ @document_repo.find_docid(docid).collect{|docid|
48
+ Document.new(persistence_for(docid), docid, @document_repo[docid], @global_annotations)
49
+ }
50
+ end
51
+
52
+ def exists?(namespace=nil, id = nil, type = nil, hash = nil)
53
+ find(namespace, id, type, hash).any?
54
+ end
55
+ end
@@ -0,0 +1,289 @@
1
+ require 'rbbt/ner/annotations'
2
+ require 'rbbt/util/tsv'
3
+ require 'rbbt/util/resource'
4
+ require 'rbbt/util/misc'
5
+ require 'json'
6
+
7
+ class Document
8
+
9
+ attr_accessor :text, :docid, :namespace, :id, :type, :hash, :annotations, :segment_indeces, :persistence_dir, :global_persistence
10
+ def initialize(persistence_dir = nil, docid = nil, text = nil, global_persistence = nil)
11
+ @annotations = {}
12
+ @segment_indeces = {}
13
+
14
+ if not persistence_dir.nil?
15
+ @persistence_dir = persistence_dir
16
+ @persistence_dir = Resource::Path.path(@persistence_dir) if not Resource::Path == @persistence_dir
17
+ end
18
+
19
+ @global_persistence = global_persistence
20
+
21
+ if not docid.nil?
22
+ @docid = docid
23
+ update_docid
24
+ end
25
+ @text = text unless text.nil?
26
+ end
27
+
28
+ def update_docid
29
+ @namespace, @id, @type, @hash = docid.split(":", -1)
30
+ end
31
+
32
+ def docid=(docid)
33
+ @docid = docid
34
+ update_docid
35
+ end
36
+
37
+ def self.save_segment(segment, fields = nil)
38
+ if fields.nil?
39
+ eend = case segment.offset; when nil; nil; when -1; -1; else segment.end; end
40
+ [segment.offset, eend, segment.info.to_json]
41
+ else
42
+ eend = case segment.offset; when nil; nil; when -1; -1; else segment.end; end
43
+ info = segment.info
44
+ info["literal"] = segment.to_s.gsub(/\s/,' ')
45
+ info.extend IndiferentHash
46
+ [segment.offset, eend].concat info.values_at(*fields.collect{|f| f.downcase}).collect{|v| Array === v ? v * "|" : v}
47
+ end
48
+ end
49
+
50
+ def self.load_segment(text, annotation, fields = nil)
51
+ if fields.nil?
52
+ start, eend, info = annotation.values_at 0,1,2
53
+ info = JSON.parse(info)
54
+ else
55
+ start, eend = annotation.values_at 0,1
56
+ info = Misc.process_to_hash(fields) do |fields| annotation.values_at(*fields.collect{|f| f.downcase}).collect{|v| v.index("|").nil? ? v : v.split("|")} end
57
+ end
58
+
59
+ Segment.load(text, start, eend, info, @docid)
60
+ end
61
+
62
+ def self.tsv(segments, fields = nil)
63
+ tsv = TSV.new({}, :list, :key => "ID", :fields => %w(Start End))
64
+ if fields.nil?
65
+ tsv.fields += ["Info"]
66
+ else
67
+ tsv.fields += fields
68
+ end
69
+
70
+ segments.each{|segment| tsv[segment.id] = Document.save_segment(segment, fields) unless segment.offset.nil?}
71
+
72
+ tsv
73
+ end
74
+
75
+
76
+ #{{{ PERSISTENCE
77
+
78
+ TSV_REPOS = {}
79
+ FIELDS_FOR_ENTITY_PERSISTENCE = {}
80
+ def self.persist(entity, fields = nil)
81
+
82
+ if not fields.nil?
83
+ fields = [fields] if not Array === fields
84
+ fields = fields.collect{|f| f.to_s}
85
+ FIELDS_FOR_ENTITY_PERSISTENCE[entity.to_s] = fields unless fields.nil?
86
+ end
87
+
88
+ self.class_eval <<-EOC
89
+ def load_with_persistence_#{entity}
90
+ fields = FIELDS_FOR_ENTITY_PERSISTENCE["#{ entity }"]
91
+
92
+ annotations = Persistence.persist("#{ entity }", :Entity, :tsv_string,
93
+ :persistence_file => File.join(@persistence_dir, "#{ entity }")) do
94
+
95
+ tsv = TSV.new({}, :list, :key => "ID", :fields => %w(Start End))
96
+ if fields.nil?
97
+ tsv.fields += ["Info"]
98
+ else
99
+ tsv.fields += fields
100
+ end
101
+
102
+ segments = produce_#{entity}
103
+ segments.each{|segment| tsv[segment.id] = Document.save_segment(segment, fields) unless segment.offset.nil?}
104
+
105
+ tsv
106
+ end
107
+
108
+ annotations.collect{|id, annotation| Document.load_segment(text, annotation, fields)}
109
+ end
110
+ EOC
111
+ end
112
+
113
+ def self.persist_in_tsv(entity, tsv = nil, fields = nil)
114
+ if not tsv.nil? and not tsv.respond_to?(:keys)
115
+ fields = tsv
116
+ tsv = nil
117
+ end
118
+
119
+ TSV_REPOS[entity.to_s] = tsv
120
+
121
+ if not fields.nil?
122
+ fields = [fields] if not Array === fields
123
+ fields = fields.collect{|f| f.to_s}
124
+ FIELDS_FOR_ENTITY_PERSISTENCE[entity.to_s] = fields unless fields.nil?
125
+ end
126
+
127
+ self.class_eval <<-EOC
128
+ def load_with_persistence_#{entity}
129
+ repo = TSV_REPOS["#{ entity }"]
130
+ if repo.nil?
131
+ raise "No persistence file or persistencr dir for persist_in_tsv" if persistence_dir.nil?
132
+ repo = TCHash.get(persistence_dir.annotations_by_type.find, TCHash::TSVSerializer)
133
+ end
134
+
135
+
136
+ fields = FIELDS_FOR_ENTITY_PERSISTENCE["#{ entity }"]
137
+
138
+ if not repo.include? "#{ entity }"
139
+ tsv = TSV.new({}, :list, :key => "ID", :fields => %w(Start End))
140
+ if fields.nil?
141
+ tsv.fields += ["Info"]
142
+ else
143
+ tsv.fields += fields
144
+ end
145
+
146
+ produce_#{entity}.each{|segment| tsv[segment.id] = Document.save_segment(segment, fields) unless segment.offset.nil?}
147
+ repo.write
148
+ repo["#{entity}"] = tsv
149
+ repo.read
150
+ end
151
+
152
+ annotations = repo["#{entity}"]
153
+
154
+ repo.close
155
+
156
+ annotations.collect{|id, annotation| Document.load_segment(text, annotation, fields)}
157
+ end
158
+ EOC
159
+ end
160
+
161
+ def self.persist_in_global_tsv(entity, tsv = nil, fields = nil, doc_field = nil, entity_field = nil)
162
+ if not tsv.nil? and not tsv.respond_to?(:keys)
163
+ entity_field = doc_field if doc_field
164
+ doc_field = fields if fields
165
+ fields = tsv if tsv
166
+ tsv = nil
167
+ end
168
+
169
+ doc_field ||= "Document ID"
170
+ entity_field ||= "Entity Type"
171
+
172
+ TSV_REPOS[entity.to_s] = tsv
173
+
174
+ if not fields.nil?
175
+ fields = [fields] if not Array === fields
176
+ fields = fields.collect{|f| f.to_s}
177
+ FIELDS_FOR_ENTITY_PERSISTENCE[entity.to_s] = fields unless fields.nil?
178
+ end
179
+
180
+ self.class_eval <<-EOC
181
+ def load_with_persistence_#{entity}
182
+ fields = FIELDS_FOR_ENTITY_PERSISTENCE["#{ entity }"]
183
+
184
+ data = TSV_REPOS["#{ entity }"]
185
+
186
+ if data.nil?
187
+ data = global_persistence
188
+ end
189
+
190
+ data.filter
191
+ data.add_filter("field:#{ doc_field }", @docid)
192
+ data.add_filter("field:#{ entity_field }", "#{ entity }")
193
+
194
+ if data.keys.empty?
195
+ tsv = TSV.new({}, :list, :key => "ID", :fields => %w(Start End))
196
+ if fields.nil?
197
+ tsv.fields += ["Info"]
198
+ else
199
+ tsv.fields += fields
200
+ end
201
+
202
+ segments = produce_#{entity}
203
+ segments << Segment.annotate("No #{entity} found in document #{ @docid }", -1) if segments.empty?
204
+ segments.each{|segment| tsv[segment.id] = Document.save_segment(segment, fields) unless segment.offset.nil?}
205
+
206
+ tsv.add_field "#{ doc_field }" do
207
+ @docid
208
+ end
209
+
210
+ tsv.add_field "#{ entity_field }" do
211
+ "#{ entity }"
212
+ end
213
+
214
+ data.write
215
+ data.merge!(tsv)
216
+ data.read
217
+ end
218
+
219
+ segments = []
220
+ data.each{|id, annotation| segments << Document.load_segment(text, annotation, fields) unless annotation[1].to_i == -1}
221
+
222
+ data.pop_filter
223
+ data.pop_filter
224
+
225
+ segments
226
+ end
227
+ EOC
228
+ end
229
+
230
+
231
+ def self.define(entity, &block)
232
+ send :define_method, "produce_#{entity}", &block
233
+
234
+ self.class_eval <<-EOC
235
+ def load_#{entity}
236
+ return if annotations.include? "#{ entity }"
237
+ if self.respond_to?("load_with_persistence_#{entity}") and not @persistence_dir.nil?
238
+ annotations["#{entity}"] = load_with_persistence_#{entity}
239
+ else
240
+ annotations["#{ entity }"] = produce_#{entity}
241
+ end
242
+ end
243
+
244
+ def #{entity}
245
+ begin
246
+ entities = annotations["#{ entity }"]
247
+ if entities.nil?
248
+ load_#{entity}
249
+ entities = annotations["#{ entity }"]
250
+ end
251
+ end
252
+
253
+ entities
254
+ end
255
+
256
+ def #{entity}_at(pos, persist = false)
257
+ segment_index("#{ entity }", persist ? File.join(@persistence_dir, 'ranges') : nil)[pos]
258
+ end
259
+
260
+ EOC
261
+ end
262
+
263
+ def segment_index(name, persistence_dir = nil)
264
+ @segment_indeces[name] ||= Segment.index(self.send(name), persistence_dir.nil? ? :memory : File.join(persistence_dir, name + '.range'))
265
+ end
266
+
267
+ def load_into(segment, *annotations)
268
+ options = annotations.pop if Hash === annotations.last
269
+ options ||= {}
270
+ if options[:persist] and not @persistence_dir.nil?
271
+ persistence_dir = File.join(@persistence_dir, 'ranges')
272
+ else
273
+ persistence_dir = nil
274
+ end
275
+
276
+ segment.extend Annotated
277
+ segment.annotations ||= {}
278
+ annotations.collect do |name|
279
+ name = name.to_s
280
+ annotations = segment_index(name, persistence_dir)[segment.range]
281
+ segment.annotations[name] = annotations
282
+ class << segment
283
+ self
284
+ end.class_eval "def #{ name }; @annotations['#{ name }']; end"
285
+ end
286
+
287
+ segment
288
+ end
289
+ end