better result prio order
[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 name =~/^#{quoted_query}/i
208         @relevance += 15 if project =~ /^openSUSE:/i
209         @relevance += 5 if project =~ /^#{quoted_query}$/i
210         @relevance += 2 if project =~ /^#{quoted_query}/i
211         @relevance -= 5 if project =~ /unstable/i
212         @relevance -= 10 if project =~ /^home:/
213         calculate_specific_relevance
214       end
215
216       def calculate_specific_relevance
217         # implement in derived classes
218       end
219     end
220
221     class Binary < Item
222       attr_reader :arch_hash
223       attr_reader :filename
224       attr_reader :arch
225
226       def initialize(key, query)
227         super(key, query)
228         @arch_hash = Hash.new
229       end
230
231       def cache_specific_data(element)
232         @filename = element.filename
233         @arch = element.arch
234       end
235       
236       def calculate_specific_relevance
237         @relevance -= 10 if name =~ /-debugsource$/
238         @relevance -= 10 if name =~ /-debuginfo$/
239         @relevance -= 3 if name =~ /-devel$/
240         @relevance -= 3 if name =~ /-doc$/
241       end
242
243       def update_description
244         @description = cache.description(self)
245         return unless @description.nil?
246         return if self.empty?
247         begin
248           bin = self[0]
249           info = ::Published.find bin.filename, :view => "fileinfo", :project => @project,
250             :repository => @repository, :arch => bin.arch.to_s
251           if info.has_element? :description
252             @description = info.description.to_s
253           else
254             @description = ""
255           end
256         rescue ActiveXML::Transport::NotFoundError
257         rescue 
258           @description = ""
259         end
260         cache.store_description(self, @description)
261         return @description
262       end
263     end
264
265     class Pattern < Item
266       attr_reader :filename
267       attr_reader :filepath
268       attr_reader :repository
269       attr_reader :type
270
271       def calculate_specific_relevance
272         # pattern bonus
273         @relevance += 20
274       end
275       
276       def cache_specific_data(element)
277         @filename = element.filename.to_s
278         @filepath = element.filepath.to_s
279         @repository = element.repository.to_s
280         @type = element.type.to_s
281       end
282
283       def update_description
284         @description = cache.description(self)
285         return unless @description.nil?
286         begin
287           pat = ::Published.find @filename, :project => @project, :repository => @repository, :view => :fileinfo
288           if pat.has_element? :description
289             @description = pat.description.to_s
290           else
291             @description = ""
292           end
293         rescue
294           @description = ""
295         end
296         cache.store_description(self, @description)
297         return @description
298       end
299     end
300
301     class Fragment < Hash
302       attr_accessor :fragment_type
303
304       def initialize(element)
305         #XXX: xml-backend specific code, change when xml-backends are
306         #XXX: switchable
307         element.data.attributes.each do |attr|
308           self[attr.name] = attr.value
309         end
310       end
311
312       def __key
313         @__key ||= @fragment_type.to_s+"|"+%w(project repository name).map{|x| self[x]}.join('|')
314       end
315
316       def dump
317         out = "<ul>"
318         each do |key,val|
319           out << "<li><b>#{key}:</b> #{val}</li>x"
320         end
321         out << "</ul>"
322         return out
323       end
324
325       def type(*args)
326         method_missing(:type,*args)
327       end
328
329       def method_missing(symbol,*args,&block)
330         if self.has_key? symbol.to_s
331           return self[symbol.to_s]
332         end
333         super(symbol,*args,&block)
334       end
335     end
336
337     class Cache
338       attr_accessor :active
339       def initialize
340         tmpdir = RAILS_ROOT+"/tmp/cache"
341         @desctmpdir = tmpdir+"/_descriptions"
342         @resulttmpdir = tmpdir+"/_searchresults"
343         @descexpire = 600.0
344         @resultexpire = 300.0
345         @active = true
346       end
347
348       def logger
349         RAILS_DEFAULT_LOGGER
350       end
351
352       def description(item)
353         fname = @desctmpdir + "/" + Digest::MD5.hexdigest(item.key)
354         return read(fname, @descexpire)
355       end
356
357       def searchresult(query, baseproject)
358         key = query + "|" + baseproject.to_s
359         fname = @resulttmpdir + "/" + Digest::MD5.hexdigest(key)
360         if str = read(fname, @resultexpire)
361           return Marshal.load(str)
362         else
363           return nil
364         end
365       end
366
367       def store_description(item, desc)
368         fname = @desctmpdir + "/" + Digest::MD5.hexdigest(item.key)
369         write(fname, desc)
370       end
371
372       def store_searchresult(query, baseproject, result)
373         key = query + "|" + baseproject.to_s
374         fname = @resulttmpdir + "/" + Digest::MD5.hexdigest(key)
375         write(fname, Marshal.dump(result))
376       end
377
378       private
379       def read(fname, exp_time_sec)
380         return nil unless @active
381         begin
382           stat = File::stat(fname)
383         rescue Errno::ENOENT
384           return nil
385         end
386
387         if (Time.now-stat.mtime) < exp_time_sec
388           logger.debug "[Seeker::SearchResult::Cache] reading #{fname} from cache"
389           return File.read(fname)
390         else
391           return nil
392         end
393       end
394
395       def write(fname, data)
396         return unless @active
397         File.open(fname,"w") do |f|
398           logger.debug "[Seeker::SearchResult::Cache] writing #{fname} to cache"
399           f.write data
400         end
401       end
402     end
403   end
404 end