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