fastri 0.1.1.1 → 0.2.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,6 +1,18 @@
1
1
 
2
2
  User-visible changes in FastRI
3
3
 
4
+ Since version 0.1.1 (2006-11-10)
5
+ ================================
6
+ Features
7
+ --------
8
+ * fri can do full-text search (-S, --full-text); try fri -S byte order
9
+ * fri can now determine where a method actually came from for core classes
10
+ e.g. fri File.inject -> docs for Enumerable#inject
11
+ * you can specify which ports the DRb services must bind to:
12
+ fastri-server -s 192.168.1.2:54321
13
+ fri -s 192.168.1.2:12345
14
+ * new search methods: "anywhere" (a) and "anywhere, case-indep." (A)
15
+
4
16
  Since version 0.1.0 (2006-11-08)
5
17
  ================================
6
18
  Features
data/Rakefile CHANGED
@@ -42,10 +42,11 @@ Spec = Gem::Specification.new do |s|
42
42
  s.summary = "RI docs across machines, faster and smarter than ri."
43
43
  s.description = <<EOF
44
44
  FastRI is an alternative to the ri command-line tool. It is *much* faster, and
45
- also allows you to offer RI lookup services over DRb. FastRI is a bit smarter
46
- than ri, and can find classes anywhere in the hierarchy without specifying the
47
- "full path". It also knows about gems, and can tell you e.g. which extensions
48
- to a core class were added by a specific gem.
45
+ also allows you to offer RI lookup services over DRb. FastRI is smarter than
46
+ ri, and can find classes anywhere in the hierarchy without specifying the
47
+ "full path". FastRI can perform fast full-text searches. It also knows about
48
+ gems, and can tell you e.g. which extensions to a core class were added by a
49
+ specific gem.
49
50
  EOF
50
51
  s.files = PKG_FILES.to_a
51
52
  s.require_path = 'lib'
data/bin/fastri-server CHANGED
@@ -1,10 +1,12 @@
1
- #! /home/batsman/usr//bin/ruby
1
+ #!/usr/bin/env ruby
2
2
  # fastri-server: serve RI documentation over DRb
3
3
  # Copyright (C) 2006 Mauricio Fernandez <mfp@acm.org>
4
4
 
5
5
  require 'fastri/version'
6
6
  require 'fastri/ri_index'
7
7
  require 'fastri/ri_service'
8
+ require 'fastri/util'
9
+ require 'fastri/full_text_indexer'
8
10
  require 'enumerator'
9
11
 
10
12
  FASTRI_SERVER_VERSION = "0.0.1"
@@ -18,25 +20,10 @@ def make_index(index_file)
18
20
  paths = [ RI::Paths::SYSDIR, RI::Paths::SITEDIR, RI::Paths::HOMEDIR ].find_all do |p|
19
21
  p && File.directory?(p)
20
22
  end
21
- begin
22
- require 'rubygems'
23
- gemdirs = Dir["#{Gem.path}/doc/*/ri"]
24
- gems = Hash.new{|h,k| h[k] = []}
25
- gemdirs.each do |path|
26
- gemname, version = %r{/([^/]+)-(.*)/ri$}.match(path).captures
27
- if gemname.nil? # doesn't follow any conventions :(
28
- gems[path[%r{/([^/]+)/ri$}, 1]] << ["unknown", path]
29
- else
30
- gems[gemname] << [version, path]
31
- end
32
- end
33
- gems.sort_by{|name, _| name}.each do |name, versions|
34
- version, path = versions.sort.last
35
- puts "Indexing RI docs for #{name} version #{version}."
36
- paths << path
37
- end
38
- rescue LoadError
39
- end
23
+ FastRI::Util.gem_directories_unique.each do |name, version, path|
24
+ paths << path
25
+ puts "Indexing RI docs for #{name} version #{version || "unknown"}."
26
+ end
40
27
 
41
28
  puts "Building index."
42
29
  t0 = Time.new
@@ -52,21 +39,59 @@ EOF
52
39
  ri_reader
53
40
  end
54
41
 
55
- # stolen from RubyGems
56
- def find_home
57
- ['HOME', 'USERPROFILE'].each do |homekey|
58
- return ENV[homekey] if ENV[homekey]
42
+ def linearize(comment)
43
+ case s = comment["body"]
44
+ when String; s
45
+ else
46
+ if Array === (y = comment["contents"])
47
+ y.map{|z| linearize(z)}.join("\n")
48
+ elsif s = comment["text"]
49
+ s
50
+ else
51
+ nil
52
+ end
53
+ end
54
+ end
55
+
56
+ def make_full_text_index(dir)
57
+ paths = [ RI::Paths::SYSDIR, RI::Paths::SITEDIR, RI::Paths::HOMEDIR ].find_all do |p|
58
+ p && File.directory?(p)
59
59
  end
60
- if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
61
- return "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}"
60
+ FastRI::Util.gem_directories_unique.each do |name, version, path|
61
+ paths << path
62
+ puts "Indexing RI docs for #{name} version #{version || "unknown"}."
62
63
  end
63
- begin
64
- File.expand_path("~")
65
- rescue StandardError => ex
66
- if File::ALT_SEPARATOR
67
- "C:/"
68
- else
69
- "/"
64
+ unless File.exist?(dir)
65
+ Dir.mkdir(dir)
66
+ end
67
+ indexer = FastRI::FullTextIndexer.new(40)
68
+ bad = 0
69
+ paths.each do |path|
70
+ Dir["#{path}/**/*.yaml"].each do |yamlfile|
71
+ yaml = File.read(yamlfile)
72
+ begin
73
+ data = YAML.load(yaml.gsub(/ \!.*/, ''))
74
+ rescue Exception
75
+ bad += 1
76
+ #puts "Couldn't load #{yamlfile}"
77
+ next
78
+ end
79
+
80
+ desc = (data['comment']||[]).map{|x| linearize(x)}.join("\n")
81
+ desc.gsub!(/<\/?(em|b|tt|ul|ol|table)>/, "")
82
+ desc.gsub!(/&quot;/, "'")
83
+ desc.gsub!(/&lt;/, "<")
84
+ desc.gsub!(/&gt;/, ">")
85
+ desc.gsub!(/&amp;/, "&")
86
+ unless desc.empty?
87
+ indexer.add_document(yamlfile, desc)
88
+ end
89
+ end
90
+ end
91
+
92
+ File.open(File.join(dir, "full_text.dat"), "wb") do |fulltextIO|
93
+ File.open(File.join(dir, "suffixes.dat"), "wb") do |suffixesIO|
94
+ indexer.build_index(fulltextIO, suffixesIO)
70
95
  end
71
96
  end
72
97
  end
@@ -75,8 +100,12 @@ end
75
100
 
76
101
  require 'optparse'
77
102
 
103
+ home = FastRI::Util.find_home
78
104
  options = {:allowed_hosts => ["127.0.0.1"], :addr => "127.0.0.1",
79
- :index_file => File.join(find_home, ".fastri-index")}
105
+ :index_file => File.join(home, ".fastri-index"),
106
+ :do_full_text => false,
107
+ :full_text_dir => File.join(home, ".fastri-fulltext"),
108
+ }
80
109
  OptionParser.new do |opts|
81
110
  opts.banner = "Usage: fastri-server.rb [options]"
82
111
 
@@ -100,6 +129,17 @@ OptionParser.new do |opts|
100
129
  exit 0
101
130
  end
102
131
 
132
+ opts.on("-F", "--full-text-dir DIR", "Place full-text index in DIR",
133
+ "(default: #{options[:full_text_dir]})") do |dir|
134
+ options[:full_text_dir] = dir if dir
135
+ options[:do_full_text] = true
136
+ end
137
+
138
+ opts.on("-B", "--rebuild-full-text", "Rebuild full-text index.") do
139
+ make_full_text_index(options[:full_text_dir])
140
+ exit 0
141
+ end
142
+
103
143
  opts.on("-h", "--help", "Show this help message") do
104
144
  puts opts
105
145
  exit
@@ -129,7 +169,9 @@ options[:allowed_hosts].each{|host| acl_opt.concat ["allow", host.strip]}
129
169
  acl = ACL.new(acl_opt)
130
170
  DRb.install_acl(acl)
131
171
 
132
- drb_addr = "druby://#{options[:addr]}:0"
172
+ ip = options[:addr][/^[^:]+/] || "127.0.0.1"
173
+ port = options[:addr][/:(\d+)/, 1] || 0
174
+ drb_addr = "druby://#{ip}:#{port}"
133
175
  DRb.start_service(drb_addr)
134
176
 
135
177
  $SAFE = 1
data/bin/fri CHANGED
@@ -1,10 +1,12 @@
1
- #! /home/batsman/usr//bin/ruby
1
+ #!/usr/bin/env ruby
2
2
  # fri: access RI documentation through DRb
3
3
  # Copyright (C) 2006 Mauricio Fernandez <mfp@acm.org>
4
4
  #
5
5
 
6
6
  require 'rinda/ring'
7
7
  require 'optparse'
8
+ require 'fastri/util'
9
+ require 'fastri/full_text_index'
8
10
 
9
11
  # we bind to 127.0.0.1 by default, because otherwise Ruby will try with
10
12
  # 0.0.0.0, which results in a DNS request, adding way too much latency
@@ -27,6 +29,8 @@ options = {
27
29
  :nested_partial, :nested_partial_ci,
28
30
  ],
29
31
  :show_matches => false,
32
+ :do_full_text => false,
33
+ :full_text_dir => File.join(FastRI::Util.find_home, ".fastri-fulltext"),
30
34
  }
31
35
  override_addr_env = false
32
36
  optparser = OptionParser.new do |opts|
@@ -41,17 +45,28 @@ optparser = OptionParser.new do |opts|
41
45
  order_mapping = {
42
46
  'e' => :exact, 'E' => :exact_ci, 'n' => :nested, 'N' => :nested_ci,
43
47
  'p' => :partial, 'P' => :partial_ci, 'x' => :nested_partial,
44
- 'X' => :nested_partial_ci
48
+ 'X' => :nested_partial_ci, 'a' => :anywhere, 'A' => :anywhere_ci,
45
49
  }
46
50
  opts.on("-O", "--order ORDER", "Specify lookup order.",
47
51
  "(default: eEnNpPxX)", "Uppercase: case-indep.",
48
- "e: exact, n: nested, p: partial",
49
- "x: nested and partial") do |order|
52
+ "e:exact n:nested p:partial (completion)",
53
+ "x:nested and partial",
54
+ "a:match method name anywhere") do |order|
50
55
  options[:lookup_order] = order.split(//).map{|x| order_mapping[x]}.compact
51
56
  end
52
57
 
53
58
  opts.on("--show-matches", "Only show matching entries."){ options[:show_matches] = true }
54
59
 
60
+ opts.on("-S", "--full-text", "Perform full-text search.") do
61
+ options[:do_full_text] = true
62
+ end
63
+
64
+ opts.on("-F", "--full-text-dir DIR", "Use full-text index in DIR",
65
+ "(default: #{options[:full_text_dir]})") do |dir|
66
+ options[:full_text_dir] = dir if dir
67
+ options[:do_full_text] = true
68
+ end
69
+
55
70
  opts.on("-f", "--format FMT", "Format to use when displaying output:",
56
71
  " ansi, plain (default: #{options[:format]})") do |format|
57
72
  options[:format] = format
@@ -74,12 +89,53 @@ if ARGV.empty?
74
89
  exit
75
90
  end
76
91
 
92
+ # {{{ try to find where the method comes from exactly
93
+ def help_method_extract(m) # :nodoc:
94
+ unless m.inspect =~ %r[\A#<(?:Unbound)?Method: (.*?)>\Z]
95
+ raise "Cannot parse result of #{m.class}#inspect: #{m.inspect}"
96
+ end
97
+ $1.sub(/\A.*?\((.*?)\)(.*)\Z/){ "#{$1}#{$2}" }.sub(/\./, "::").sub(/#<Class:(.*?)>#/) { "#{$1}::" }
98
+ end
99
+
100
+ def magic_help(query)
101
+ if query =~ /\A(.*?)(#|::|\.)(.*)\Z/
102
+ c, k, m = $1, $2, $3
103
+ begin
104
+ c = Object.const_get(c)
105
+ m = case k
106
+ when "#"
107
+ c.instance_method(m)
108
+ when "::"
109
+ c.method(m)
110
+ when "."
111
+ begin
112
+ c.method(m)
113
+ rescue NameError
114
+ c.instance_method(m)
115
+ end
116
+ end
117
+ help_method_extract(m)
118
+ rescue Exception
119
+ query
120
+ end
121
+ else
122
+ query
123
+ end
124
+ end
125
+ help_query = magic_help(ARGV[0])
126
+
127
+ #{{{ determine the address to bind to
77
128
  if override_addr_env
78
- addr = "druby://#{options[:addr]}:0"
129
+ addr_spec = options[:addr]
79
130
  else
80
- addr = "druby://#{ENV["FASTRI_ADDR"]||options[:addr]}:0"
131
+ addr_spec = ENV["FASTRI_ADDR"] || options[:addr]
81
132
  end
82
133
 
134
+ ip = addr_spec[/^[^:]+/] || "127.0.0.1"
135
+ port = addr_spec[/:(\d+)/, 1] || 0
136
+ addr = "druby://#{ip}:#{port}"
137
+
138
+ #{{{ start DRb and perform request
83
139
  begin
84
140
  DRb.start_service(addr)
85
141
  ring_server = Rinda::RingFinger.primary
@@ -103,9 +159,90 @@ info_options = {
103
159
  :width => options[:width],
104
160
  :lookup_order => options[:lookup_order],
105
161
  }
162
+
163
+ MAX_CONTEXT_LINES = 20
164
+ def context_wrap(text, width)
165
+ "... " +
166
+ text.gsub(/(.{1,#{width-4}})( +|$\n?)|(.{1,#{width-4}})/, "\\1\\3\n").chomp
167
+ end
168
+
169
+ def display_fulltext_search_results(results, gem_dir_info = FastRI::Util.gem_directories_unique,
170
+ width = 78)
171
+ return if results.empty?
172
+ path = File.expand_path(results[0].path)
173
+ gem_name, version, gem_path = FastRI::Util.gem_info_for_path(path, gem_dir_info)
174
+ if gem_name
175
+ rel_path = path[/#{Regexp.escape(gem_path)}\/(.*)/, 1]
176
+ if rel_path
177
+ entry_name = FastRI::Util.gem_relpath_to_full_name(rel_path)
178
+ end
179
+ puts "Found in #{gem_name} #{version} #{entry_name}"
180
+ else
181
+ rdoc_system_path = File.expand_path(RI::Paths::SYSDIR)
182
+ if path.index(rdoc_system_path)
183
+ rel_path = path[/#{Regexp.escape(rdoc_system_path)}\/(.*)/, 1]
184
+ puts "Found in system #{FastRI::Util.gem_relpath_to_full_name(rel_path)}"
185
+ else
186
+ puts "Found in #{path}:"
187
+ end
188
+ end
189
+ text = results.map do |result|
190
+ context = result.context(120)
191
+ from = (context.rindex("\n", context.index(result.query)) || -1) + 1
192
+ to = (context.index("\n", context.index(result.query)) || 0) - 1
193
+ context_wrap(context[from..to], width)
194
+ end
195
+ puts
196
+ puts text.uniq[0...MAX_CONTEXT_LINES]
197
+ puts
198
+ end
199
+
200
+ if options[:do_full_text]
201
+ fulltext = File.join(options[:full_text_dir], "full_text.dat")
202
+ suffixes = File.join(options[:full_text_dir], "suffixes.dat")
203
+ begin
204
+ index = FastRI::FullTextIndex.new_from_filenames(fulltext, suffixes)
205
+ rescue Exception
206
+ puts <<EOF
207
+ Couldn't open the full-text index:
208
+ #{fulltext}
209
+ #{suffixes}
210
+
211
+ The index needs to be rebuilt with
212
+ fastri-server -B
213
+ EOF
214
+ exit(-1)
215
+ end
216
+ gem_dir_info = FastRI::Util.gem_directories_unique
217
+ match_sets = ARGV.map do |query|
218
+ result = index.lookup(query)
219
+ if result
220
+ index.next_matches(result) + [result]
221
+ else
222
+ []
223
+ end
224
+ end
225
+ path_map = Hash.new{|h,k| h[k] = []}
226
+ match_sets.each{|matches| matches.each{|m| path_map[m.path] << m} }
227
+ paths = match_sets[1..-1].inject(match_sets[0].map{|x| x.path}.uniq) do |s,x|
228
+ s & x.map{|y| y.path}.uniq
229
+ end
230
+ if paths.empty?
231
+ puts "nil"
232
+ else
233
+ puts "#{paths.size} hits"
234
+ paths.sort_by{|path| -path_map[path].size}.map do |path|
235
+ puts "=" * options[:width]
236
+ display_fulltext_search_results(path_map[path], gem_dir_info, options[:width])
237
+ end
238
+ end
239
+
240
+ exit 0
241
+ end
242
+
106
243
  if options[:show_matches]
107
- puts service.matches(ARGV[0], info_options).sort
244
+ puts service.matches(help_query, info_options).sort
108
245
  else
109
- puts service.info(ARGV[0], info_options)
246
+ puts service.info(help_query, info_options)
110
247
  end
111
248
  # vi: set sw=2 expandtab:
data/bin/ri-emacs CHANGED
@@ -1,4 +1,4 @@
1
- #! /home/batsman/usr//bin/ruby
1
+ #!/usr/bin/env ruby
2
2
  ## drop-in replacement for the ri-emacs helper script for use
3
3
  # with ri-ruby.el, using the FastRI service via DRb
4
4
  #
@@ -0,0 +1,245 @@
1
+ # Copyright (C) 2006 Mauricio Fernandez <mfp@acm.org>
2
+ #
3
+
4
+ require 'fastri/full_text_indexer'
5
+ require 'stringio'
6
+
7
+ module FastRI
8
+
9
+ class FullTextIndex
10
+ MAX_QUERY_SIZE = 20
11
+ MAX_REGEXP_MATCH_SIZE = 255
12
+ class Result
13
+ attr_reader :path, :query, :index, :metadata
14
+
15
+ def initialize(searcher, query, index, path, metadata)
16
+ @searcher = searcher
17
+ @index = index
18
+ @query = query
19
+ @path = path
20
+ @metadata = metadata
21
+ end
22
+
23
+ def context(size)
24
+ @searcher.fetch_data(@index, 2*size+1, -size)
25
+ end
26
+
27
+ def text(size)
28
+ @searcher.fetch_data(@index, size, 0)
29
+ end
30
+ end
31
+
32
+ class << self; private :new end
33
+
34
+ DEFAULT_OPTIONS = {
35
+ :max_query_size => MAX_QUERY_SIZE,
36
+ }
37
+
38
+ def self.new_from_ios(fulltext_IO, suffix_arrray_IO, options = {})
39
+ new(:io, fulltext_IO, suffix_arrray_IO, options)
40
+ end
41
+
42
+ def self.new_from_filenames(fulltext_fname, suffix_arrray_fname, options = {})
43
+ new(:filenames, fulltext_fname, suffix_arrray_fname, options)
44
+ end
45
+
46
+ attr_reader :max_query_size
47
+ def initialize(type, fulltext, sarray, options)
48
+ options = DEFAULT_OPTIONS.merge(options)
49
+ case type
50
+ when :io
51
+ @fulltext_IO = fulltext
52
+ @sarray_IO = sarray
53
+ when :filenames
54
+ @fulltext_fname = fulltext
55
+ @sarray_fname = sarray
56
+ else raise "Unknown type"
57
+ end
58
+ @type = type
59
+ @max_query_size = options[:max_query_size]
60
+ check_magic
61
+ end
62
+
63
+ def lookup(term)
64
+ get_fulltext_IO do |fulltextIO|
65
+ get_sarray_IO do |sarrayIO|
66
+ case sarrayIO
67
+ when StringIO
68
+ num_suffixes = sarrayIO.string.size / 4 - 1
69
+ else
70
+ num_suffixes = sarrayIO.stat.size / 4 - 1
71
+ end
72
+
73
+ index, offset = binary_search(sarrayIO, fulltextIO, term, 0, num_suffixes)
74
+ if offset
75
+ fulltextIO.pos = offset
76
+ path, metadata = find_metadata(fulltextIO)
77
+ return Result.new(self, term, index, path, metadata) if path
78
+ else
79
+ nil
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def next_match(result, term_or_regexp = "")
86
+ case term_or_regexp
87
+ when String; size = [result.query.size, term_or_regexp.size].max
88
+ when Regexp; size = MAX_REGEXP_MATCH_SIZE
89
+ end
90
+ get_fulltext_IO do |fulltextIO|
91
+ get_sarray_IO do |sarrayIO|
92
+ idx = result.index
93
+ loop do
94
+ idx += 1
95
+ str = get_string(sarrayIO, fulltextIO, idx, size)
96
+ upto = str.index("\0")
97
+ str = str[0, upto] if upto
98
+ break unless str.index(result.query) == 0
99
+ if str[term_or_regexp]
100
+ fulltextIO.pos = index_to_offset(sarrayIO, idx)
101
+ path, metadata = find_metadata(fulltextIO)
102
+ return Result.new(self, result.query, idx, path, metadata) if path
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ def next_matches(result, term_or_regexp = "")
110
+ case term_or_regexp
111
+ when String; size = [result.query.size, term_or_regexp.size].max
112
+ when Regexp; size = MAX_REGEXP_MATCH_SIZE
113
+ end
114
+ ret = []
115
+ get_fulltext_IO do |fulltextIO|
116
+ get_sarray_IO do |sarrayIO|
117
+ idx = result.index
118
+ loop do
119
+ idx += 1
120
+ str = get_string(sarrayIO, fulltextIO, idx, size)
121
+ upto = str.index("\0")
122
+ str = str[0, upto] if upto
123
+ break unless str.index(result.query) == 0
124
+ if str[term_or_regexp]
125
+ fulltextIO.pos = index_to_offset(sarrayIO, idx)
126
+ path, metadata = find_metadata(fulltextIO)
127
+ ret << Result.new(self, result.query, idx, path, metadata) if path
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ ret
134
+ end
135
+
136
+ def fetch_data(index, size, offset = 0)
137
+ raise "Bad offset" unless offset <= 0
138
+ get_fulltext_IO do |fulltextIO|
139
+ get_sarray_IO do |sarrayIO|
140
+ base = index_to_offset(sarrayIO, index)
141
+ actual_offset = offset
142
+ newsize = size
143
+ if base + offset < 0 # at the beginning
144
+ excess = (base + offset).abs # remember offset is < 0
145
+ newsize = size - excess
146
+ actual_offset = offset + excess
147
+ end
148
+ str = get_string(sarrayIO, fulltextIO, index, newsize, offset)
149
+ from = (str.rindex("\0", -actual_offset) || -1) + 1
150
+ to = (str.index("\0", -actual_offset) || 0) - 1
151
+ str[from..to]
152
+ end
153
+ end
154
+ end
155
+
156
+ private
157
+ def check_magic
158
+ get_fulltext_IO do |io|
159
+ io.rewind
160
+ header = io.read(FullTextIndexer::MAGIC.size)
161
+ raise "Unsupported index format." unless header
162
+ version = header[/\d+\.\d+\.\d+/]
163
+ raise "Unsupported index format." unless version
164
+ major, minor, teeny = version.scan(/\d+/)
165
+ if major != FASTRI_FT_INDEX_FORMAT_MAJOR or
166
+ minor > FASTRI_FT_INDEX_FORMAT_MINOR
167
+ raise "Unsupported index format"
168
+ end
169
+ end
170
+ end
171
+
172
+ def get_fulltext_IO
173
+ case @type
174
+ when :io; yield @fulltext_IO
175
+ when :filenames
176
+ File.open(@fulltext_fname, "rb"){|f| yield f}
177
+ end
178
+ end
179
+
180
+ def get_sarray_IO
181
+ case @type
182
+ when :io; yield @sarray_IO
183
+ when :filenames
184
+ File.open(@sarray_fname, "rb"){|f| yield f}
185
+ end
186
+ end
187
+
188
+ def index_to_offset(sarrayIO, index)
189
+ sarrayIO.pos = index * 4
190
+ sarrayIO.read(4).unpack("V")[0]
191
+ end
192
+
193
+ def find_metadata(fulltextIO)
194
+ oldtext = ""
195
+ loop do
196
+ text = fulltextIO.read(4096)
197
+ break unless text
198
+ if idx = text.index("\0")
199
+ if idx + 4 >= text.size
200
+ text.concat(fulltextIO.read(4096))
201
+ end
202
+ len = text[idx+1, 4].unpack("V")[0]
203
+ missing = idx + 5 + len - text.size
204
+ if missing > 0
205
+ text.concat(fulltextIO.read(missing))
206
+ end
207
+ footer = text[idx + 5, len - 1]
208
+ path, metadata = /(.*?)\0(.*)/m.match(footer).captures
209
+ return [path, Marshal.load(metadata)]
210
+ end
211
+ oldtext = text
212
+ end
213
+ nil
214
+ end
215
+
216
+ def get_string(sarrayIO, fulltextIO, index, size, off = 0)
217
+ sarrayIO.pos = index * 4
218
+ offset = sarrayIO.read(4).unpack("V")[0]
219
+ fulltextIO.pos = [offset + off, 0].max
220
+ fulltextIO.read(size)
221
+ end
222
+
223
+ def binary_search(sarrayIO, fulltextIO, term, from, to)
224
+ #puts "BINARY #{from} -- #{to}"
225
+ #left = get_string(sarrayIO, fulltextIO, from, @max_query_size)
226
+ #right = get_string(sarrayIO, fulltextIO, to, @max_query_size)
227
+ #puts " #{left.inspect} -- #{right.inspect}"
228
+ middle = (from + to) / 2
229
+ pivot = get_string(sarrayIO, fulltextIO, middle, @max_query_size)
230
+ if from == to
231
+ if pivot.index(term) == 0
232
+ sarrayIO.pos = middle * 4
233
+ [middle, sarrayIO.read(4).unpack("V")[0]]
234
+ else
235
+ nil
236
+ end
237
+ elsif term <= pivot
238
+ binary_search(sarrayIO, fulltextIO, term, from, middle)
239
+ elsif term > pivot
240
+ binary_search(sarrayIO, fulltextIO, term, middle+1, to)
241
+ end
242
+ end
243
+ end # class FullTextIndex
244
+
245
+ end # module FastRI