basic support to search only in a special project
[opensuse:software-o-o.git] / app / models / seeker.rb
1 require 'md5'
2
3 class Seeker < ActiveXML::Base
4   def size
5     len = data.children.length
6     #if len == 1 and data.children[0].kind_of? XML::Smart::Dom::Text
7     #  return 0
8     #end
9     return len
10   end
11
12   def self.prepare_result(query, baseproject=nil, project=nil, exclude_filter=nil, exclude_debug=false)
13     cache_key = query
14     cache_key += "_#{baseproject}" if baseproject
15     cache_key += "_#{exclude_filter}" if exclude_filter
16     cache_key += "_#{exclude_debug}" if exclude_debug
17     cache_key += "_#{project}" if project
18     cache_key = 'searchresult_' + MD5::md5( cache_key ).to_s
19     Rails.cache.fetch(cache_key, :expires_in => 10.minutes) do
20       SearchResult.search(query, baseproject, project, exclude_filter, exclude_debug)
21     end
22   end
23
24
25   class SearchResult < Array
26     def self.search(query, baseproject, project=nil, exclude_filter=nil, exclude_debug=false)
27
28       words = query.split(" ").select {|part| !part.match(/^[0-9_\.-]+$/) }
29       versions = query.split(" ").select {|part| part.match(/^[0-9_\.-]+$/) }
30       logger.debug "splitted words and version: #{words.inspect} #{versions.inspect} "
31       raise "Please provide a valid search term" if words.blank?
32
33       xpath = "contains-ic(@name, " + words.map{|word| "'#{word}'"}.join(", ") + ")"
34       xpath += ' and ' + versions.map {|part| "starts-with(@version,'#{part}')"}.join(" and ") unless versions.blank?
35       xpath += " and path/project='#{baseproject}'" unless baseproject.blank?
36       xpath += " and @project = '#{project}' " unless project.blank?
37       xpath += " and not(contains-ic(@name, '-debuginfo')) and not(contains-ic(@name, '-debugsource'))" if exclude_debug
38       #xpath += " and not(contains-ic(@project, '#{exclude_filter}'))" unless exclude_filter.blank?
39
40       bin = Seeker.find :binary, :match => xpath
41       pat = Seeker.find :pattern, :match => xpath
42       result = new(query)
43       result.add_patlist(pat)
44       result.add_binlist(bin)
45
46       # remove this hack when the backend can filter for project names
47       result.reject!{|res| /#{exclude_filter}/.match( res.project ) } unless exclude_filter.blank?
48
49       result.sort! {|x,y| y.relevance <=> x.relevance}
50
51       return result
52     end
53
54     def self.cache
55       @cache ||= Cache.new
56     end
57
58     def inspect
59       "<Seeker::Searchresult ##{object_id} @length=#{size}>"
60     end
61
62     def self.logger
63       RAILS_DEFAULT_LOGGER
64     end
65
66     attr_reader :query
67     attr_reader :binary_count
68     attr_reader :pattern_count
69     attr_accessor :page_length
70
71     def initialize(query)
72       @query = query
73       @binary_count = 0
74       @pattern_count = 0
75       @page_length = 10
76       super()
77     end
78
79     # page index starts with 1
80     def page(idx)
81       return [] if idx > page_count
82       page = self[@page_length*(idx-1),@page_length]
83       page.each do |item|
84         item.update_description
85       end
86       return page
87     end
88
89     def page_count
90       #logger.debug "[SearchResult] calculating page_count: self.length: #{self.length}, @page_length: #@page_length"
91       ((self.length-1)/@page_length)+1
92     end
93
94     def add_binlist(binlist)
95       @index = Hash.new
96       @binary_count = 0
97       binlist.each_binary do |bin|
98         @binary_count += 1
99         fragment = Fragment.new(bin)
100         fragment.fragment_type = :binary
101         add_fragment(fragment)
102       end
103     end
104
105     def add_patlist(patlist)
106       @index = Hash.new
107       @pattern_count = 0
108       patlist.each_pattern do |pat|
109         @pattern_count += 1
110         fragment = Fragment.new(pat)
111         fragment.fragment_type = :pattern
112         add_fragment(fragment)
113       end
114     end
115
116     def add_fragment(fragment)
117       key = fragment.__key
118       if @index.has_key? key
119         item = @index[key]
120       else
121         case fragment.fragment_type
122         when :binary
123           item = Binary.new(key, @query)
124         when :pattern
125           item = Pattern.new(key, @query)
126         end
127         @index[key] = item
128         self << item
129       end
130       item.add_entry(fragment)
131       return item
132     end
133
134     def dump
135       out = "<ul>"
136       each do |item|
137         out << "<li>#{item.key} #{item.dump}</li>"
138       end
139       out << "</ul>"
140     end
141
142     class Item < Array
143       attr_accessor :query
144       attr_reader :key
145       attr_reader :name
146       attr_reader :project
147       attr_reader :repository
148       attr_reader :ymp_link
149       attr_reader :description
150       attr_reader :short_description
151       attr_reader :relevance
152
153       def initialize(key, query)
154         @key = key
155         @query = query
156         @relevance = 0
157       end
158
159       def cache
160         SearchResult.cache
161       end
162
163       def inspect
164         "<#{self.class.name} ##{object_id} @length=#{size}>"
165       end
166
167       def add_entry(element)
168         if element.__key != @key
169           raise "key mismatch: #{element.__key} != #@key"
170         end
171         self << element
172         cache_data(element) unless @data_cached
173         calculate_relevance unless @relevance_calculated
174       end
175
176       def dump
177         out = "<ul><li><b>Relevance:</b> #@relevance</li>"
178         each do |bin|
179           out << "<li>#{bin.filename} #{bin.dump}</li>"
180         end
181         out << "</ul>"
182         return out
183       end
184
185       def update_description
186         # implement in derived classes
187       end
188       
189       private
190
191       def cache_data(element)
192         @project = element.project
193         @repository = element.repository
194         @name = element.name
195         cache_specific_data(element)
196         @data_cached = true
197       end
198
199       def cache_specific_data(element)
200         # implement in derived classes
201       end
202
203       def calculate_relevance
204         quoted_query = Regexp.quote @query
205         @relevance_calculated = true
206         @relevance += 15 if name =~/^#{quoted_query}$/i
207         @relevance += 5 if project =~ /^#{quoted_query}$/i
208         @relevance += 5 if name =~ /#{quoted_query}/i
209         @relevance += 2 if project =~ /#{quoted_query}/i
210         @relevance += 1 if project =~ /^openSUSE/i
211
212         @relevance -= 10 if project =~ /^home:/
213         
214         calculate_specific_relevance
215       end
216
217       def calculate_specific_relevance
218         # implement in derived classes
219       end
220     end
221
222     class Binary < Item
223       attr_reader :arch_hash
224       attr_reader :filename
225       attr_reader :arch
226
227       def initialize(key, query)
228         super(key, query)
229         @arch_hash = Hash.new
230       end
231
232       def cache_specific_data(element)
233         @filename = element.filename
234         @arch = element.arch
235       end
236       
237       def calculate_specific_relevance
238         @relevance -= 10 if name =~ /-debugsource$/
239         @relevance -= 10 if name =~ /-debuginfo$/
240         @relevance -= 3 if name =~ /-devel$/
241         @relevance -= 3 if name =~ /-doc$/
242       end
243
244       def update_description
245         @description = cache.description(self)
246         return unless @description.nil?
247         return if self.empty?
248         begin
249           bin = self[0]
250           info = ::Published.find bin.filename, :view => "fileinfo", :project => @project,
251             :repository => @repository, :arch => bin.arch.to_s
252           if info.has_element? :description
253             @description = info.description.to_s
254           else
255             @description = ""
256           end
257         rescue ActiveXML::Transport::NotFoundError
258         rescue 
259           @description = ""
260         end
261         cache.store_description(self, @description)
262         return @description
263       end
264     end
265
266     class Pattern < Item
267       attr_reader :filename
268       attr_reader :filepath
269       attr_reader :repository
270       attr_reader :type
271
272       def calculate_specific_relevance
273         # pattern bonus
274         @relevance += 20
275       end
276       
277       def cache_specific_data(element)
278         @filename = element.filename.to_s
279         @filepath = element.filepath.to_s
280         @repository = element.repository.to_s
281         @type = element.type.to_s
282       end
283
284       def update_description
285         @description = cache.description(self)
286         return unless @description.nil?
287         begin
288           pat = ::Published.find @filename, :project => @project, :repository => @repository, :view => :fileinfo
289           if pat.has_element? :description
290             @description = pat.description.to_s
291           else
292             @description = ""
293           end
294         rescue
295           @description = ""
296         end
297         cache.store_description(self, @description)
298         return @description
299       end
300     end
301
302     class Fragment < Hash
303       attr_accessor :fragment_type
304
305       def initialize(element)
306         #XXX: xml-backend specific code, change when xml-backends are
307         #XXX: switchable
308         element.data.attributes.each do |attr|
309           self[attr.name] = attr.value
310         end
311       end
312
313       def __key
314         @__key ||= @fragment_type.to_s+"|"+%w(project repository name).map{|x| self[x]}.join('|')
315       end
316
317       def dump
318         out = "<ul>"
319         each do |key,val|
320           out << "<li><b>#{key}:</b> #{val}</li>x"
321         end
322         out << "</ul>"
323         return out
324       end
325
326       def type(*args)
327         method_missing(:type,*args)
328       end
329
330       def method_missing(symbol,*args,&block)
331         if self.has_key? symbol.to_s
332           return self[symbol.to_s]
333         end
334         super(symbol,*args,&block)
335       end
336     end
337
338     class Cache
339       attr_accessor :active
340       def initialize
341         tmpdir = RAILS_ROOT+"/tmp/cache"
342         @desctmpdir = tmpdir+"/_descriptions"
343         @resulttmpdir = tmpdir+"/_searchresults"
344         @descexpire = 600.0
345         @resultexpire = 300.0
346         @active = true
347       end
348
349       def logger
350         RAILS_DEFAULT_LOGGER
351       end
352
353       def description(item)
354         fname = @desctmpdir + "/" + Digest::MD5.hexdigest(item.key)
355         return read(fname, @descexpire)
356       end
357
358       def searchresult(query, baseproject)
359         key = query + "|" + baseproject.to_s
360         fname = @resulttmpdir + "/" + Digest::MD5.hexdigest(key)
361         if str = read(fname, @resultexpire)
362           return Marshal.load(str)
363         else
364           return nil
365         end
366       end
367
368       def store_description(item, desc)
369         fname = @desctmpdir + "/" + Digest::MD5.hexdigest(item.key)
370         write(fname, desc)
371       end
372
373       def store_searchresult(query, baseproject, result)
374         key = query + "|" + baseproject.to_s
375         fname = @resulttmpdir + "/" + Digest::MD5.hexdigest(key)
376         write(fname, Marshal.dump(result))
377       end
378
379       private
380       def read(fname, exp_time_sec)
381         return nil unless @active
382         begin
383           stat = File::stat(fname)
384         rescue Errno::ENOENT
385           return nil
386         end
387
388         if (Time.now-stat.mtime) < exp_time_sec
389           logger.debug "[Seeker::SearchResult::Cache] reading #{fname} from cache"
390           return File.read(fname)
391         else
392           return nil
393         end
394       end
395
396       def write(fname, data)
397         return unless @active
398         File.open(fname,"w") do |f|
399           logger.debug "[Seeker::SearchResult::Cache] writing #{fname} to cache"
400           f.write data
401         end
402       end
403     end
404   end
405 end