fastri 0.1.1.1 → 0.2.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.
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