# Basis-Klasse für das Handling von SOLR-Indizies (sogenannte SOLR-Cores) require 'rsolr' class MultiSolr::SingleCoreHandler attr_reader :solr_url # String mit der Base-Solr-Url (ohne Core-Anteil!) attr_accessor :core_name # der zu verwendende SOLR-Core attr_accessor :result_class # die zu nutzende Klasse für das Suchergebnis attr_accessor :facet_enum_fields # Array mit den Namen der Felder, die die Facet-Enum-Methode für Facets nutzen (Felder mit kleinen Wertebereichen) attr_accessor :default_search_options # Hash mit weiteren Optionen für Query Anfragen (fieldlist,..) siehe params bei der Methode search # Erzeugen neuen Core-Handler # Parameter: # solr_url : String mit der Base-Solr-Url (ohne Core-Anteil!) # core_name : der zu verwendende SOLR-Core # options: optionaler Hash mit folgenden Werten: # :result_class : die zu nutzende Klasse für das Suchergebnis, default ist MultiSolr:SearchResult # :facet_enum_fields : Array mit den Namen der Felder, # die die Facet-Enum-Methode für Facets nutzen # (Felder mit kleinen Wertebereichen) # :default_search_options : Hash mit weiteren Optionen für Query Anfragen: # :fieldlist Field-List (siehe Solr-Doku fl-Parameter), default ist alle Felder # :facets_only wenn true, dann nur Facets ermittlen # :without_facets wenn true, dann auf Facets verzichten # :result_format Rückgabe-Format (json, xml), # wenn nicht gesetzt wird "ruby" geliefert; # Ist es gesetzt wird das Ergebnis nicht geparst (also raw als String zurückgegeben) def initialize solr_url, core_name, options=nil @solr_url = solr_url || raise("No 'solr_url' given!") @core_name = core_name if options options.each do |k,v| self.send("#{k}=", v) end end @result_class ||= MultiSolr::SearchResult @default_search_options ||= {} end # Liefert RSolr-Connection (RSolr.connect) zum konfigurierten SOLR-Server(see solr_url) # und konfiguriertem Core (see core_name) # Params: # options: optionale Connect-Parameters, see Rsolr.connect def solr_connection options=nil url = "#{@solr_url}/#{@core_name}" MultiSolr.logger.debug("solr_connection: url==#{url}") if MultiSolr.logger.debug? connect_params = {:url => url} connect_params.merge!(options) if options RSolr.connect connect_params end # sendet Ping an SOLR-Instance # returns true if ok def ping self.solr_connection.head("admin/ping").response[:status] == 200 end # Liefert zu dem spezifizierten Kontext und Feld die möglichen Werte # Parameter: # fieldname: Name des Feldes (als Symbol) # context: optionales Object mit weiteren Context-Informationen, dieser wird an den Force-Query-Builder übergeben # search_request: optional bestehendes SearchRequest (für DrillDown-Funktionalität) # config: Config-Data, used as optional parameter for query-build # Beispiel: # searcher.list_possible_values :lkz, :whs_id => 5 # def list_possible_values fieldname, context=nil, search_request=nil, config=nil value_pair_data = list_possible_values_with_count fieldname, context, search_request, config values = [] # es werden nur die Werte benötigt und deshalb aus den [value,count]-Paaren entnommen value_pair_data.each do |value_count_pair| values << value_count_pair[0] end # Ergebnis ist ein Array mit Werten values end # Liefert zu dem spezifizierten Kontext und Feld die möglichen Werte und die dazugehörige Anzahl an Treffern. # Parameter: # fieldname: Name des Feldes (als Symbol) # context: optionales Object mit weiteren Context-Informationen, dieser wird an den Force-Query-Builder übergeben # search_request: optional bestehendes SearchRequest (für DrillDown-Funktionalität) # config: Config-Data, used as optional parameter for query-build # Beispiel: # searcher.list_possible_values :lkz, :whs_id => 5 # def list_possible_values_with_count fieldname, context=nil, search_request=nil, config=nil search_request ||= MultiSolr::SearchRequest.new search_request.facets = [fieldname] result = search(search_request, :context => context, :facets_only => true, :config => config) facet_counts = result.facet_counts values = {} if MultiSolr.logger.debug? MultiSolr.logger.debug "#{self.class.name}.list_possible_values_with_count(#{fieldname}, #{context.inspect}, #{search_request.inspect}): facet_counts=#{facet_counts.inspect}" end if facet_counts value_pairs = facet_counts[fieldname.to_s] if value_pairs && !value_pairs.empty? # das value_pairs besteht Paar-weise aus Value und Anzahl # Es werden hier nur die Paare gebraucht, wo die Anzahl >0 ist only_int = true value_pairs.each do |value_count_pair| val, count = value_count_pair if val && count > 0 only_int = false if only_int && !val.match(/^[0-9]+$/) # Value und Anzahl in die Ergebnis-Hash schreiben values[val] = count end end # wenn val nur aus Zahlen besteht, werden die keys nach Integer konvertiert (wegen der Sortierung) values = Hash[values.map {|k, v| [k.to_i, v] }] if only_int value_pair_data = values.sort_by { |value, count| value } end end if MultiSolr.logger.debug? MultiSolr.logger.debug "#{self.class.name}.list_possible_values_with_count(#{fieldname}, #{context.inspect}) => #{values.inspect}" end # Ergebnis ist ein Array mit Arrays bestehend aus [value,count]-Paaren: [[value, count], [value,count],..] value_pair_data end # Ermittelt und cached die möglichen Werte der als pre_cache_value_fields definierten Felder # Diese werden im Rails-Cache zwischengespeichert # Parameter: # fieldname: Name des Feldes (als Symbol) # context: optionales Object mit weiteren Context-Informationen, dieser wird an den Force-Query-Builder übergeben # config: Config-Data, used as optional parameter for query-build # expires_in: optional, Gültigkeit des Cache-Wertes in Sekunden, default ist 3 Stunden # def cached_list_possible_values fieldname, context=nil, config=nil, expires_in=3.hours cache_key = "solr-#{self.core_name}-#{context}-#{fieldname}-values" values = MultiSolr.cache.read(cache_key) if values.nil? # dann sind noch gar keine Werte gecached => diese holen values = list_possible_values fieldname, context, nil, config # und nun im Cache ablegen MultiSolr.logger.debug "#{self.class.name}.cached_list_possible_values: write in cache '#{cache_key}' => #{values.inspect}" if MultiSolr.logger.debug? MultiSolr.cache.write(cache_key, values, :expires_in => expires_in) end values end # Ermittelt und cached die möglichen Werte mit Anzahl der als pre_cache_value_fields definierten Felder # Diese werden im Rails-Cache zwischengespeichert # Parameter: # fieldname: Name des Feldes (als Symbol) # context: optionales Object mit weiteren Context-Informationen, dieser wird an den Force-Query-Builder übergeben # config: Config-Data, used as optional parameter for query-build # expires_in: optional, Gültigkeit des Cache-Wertes in Sekunden, default ist 3 Stunden # def cached_list_possible_values_with_count fieldname, context=nil, config=nil, expires_in=3.hours cache_key = "solr-#{self.core_name}-#{context}-#{fieldname}-values-with-count" values = MultiSolr.cache.read(cache_key) if values.nil? # dann sind noch gar keine Werte gecached => diese holen values = list_possible_values_with_count fieldname, context, nil, config # und nun im Cache ablegen MultiSolr.logger.debug "#{self.class.name}.cached_list_possible_values_with_count: write in cache '#{cache_key}' => #{values.inspect}" if MultiSolr.logger.debug? MultiSolr.cache.write(cache_key, values, :expires_in => expires_in) end values end # ermitteln Zeitstempel des letzten Datenimports def import_status solr_core_import_handler_propfile_path MultiSolr.cache.fetch("Solr.import_status.#{self.core_name}.#{solr_core_import_handler_propfile_path}", :expires_in => 1.hours) do result = I18n.t(:unknown, :default => 'n.a.') begin raise("Solr-Import-Status-Propertiesfile not exist") unless File.exist?(solr_core_import_handler_propfile_path) matcher = /^last_index_time=(.*)$/ data = File.read(solr_core_import_handler_propfile_path) match = matcher.match(data) if match result = match[1] result.gsub!('\\', '') # Zeit enthält \ vor : result = result[0...-3] # Sekunden entfernen end rescue => ex MultiSolr.logger.warn "SolrSearch.import_status: source=#{solr_core_import_handler_propfile_path}, error=#{ex.message}\n\t"+ex.backtrace.join("\n\t") end MultiSolr.logger.info "SolrSearch.import_status: #{solr_core_import_handler_propfile_path}, result=#{result}" result end end # liefert einzelnes Solr-Dokument an Hand der Id # Parameter: # id: die gewünschte Id (Integer oder String) # id_field_name : optional, der Name des ID-Fields, default ist 'id' def get_doc_by_id(id, id_field_name=:id) solr_result = solr_connection.get 'select', :params => {:q => "#{id_field_name}:#{id}", :rows => 1} docs = solr_result['response']['docs'] return nil if docs.nil? || docs.empty? docs.first end # Ausführen der Suche / Recherche # params: # solr_search_request : die Suchanfrage als SolrSearchRequest-Instance # options : Hash mit weiteren Optionen für Query Anfragen: # :context Hash mit weiteren Context-Informationen, dieser wird u.a. an den Force-Query-Builder übergeben # :config Config-Data, uses as optional parameter for query-build # :fieldlist Field-List (siehe Solr-Doku fl-Parameter), default ist * # :facets_only wenn true, dann nur Facets ermittlen # :without_facets wenn true, dann auf Facets verzichten # :result_format Rückgabe-Format (json, xml), # wenn nicht gesetzt wird "ruby" geliefert; # Ist es gesetzt wird das Ergebnis nicht geparst (also raw als String zurückgegeben) def search solr_search_request, options=nil used_options = @default_search_options.clone used_options.merge! options if options solr_params = build_solr_params solr_search_request, used_options solr_result = solr_connection.post 'select', :params => solr_params # RAW-Result liefern wenn Result ein String ist return solr_result if solr_result.is_a? String # Parsen des Ergebnisses result = self.result_class.new solr_result, solr_search_request, used_options[:context] result end # Bilden der SOLR-Parameter für eine SOLR-Anfrage # params: # solr_search_request: die Suchanfrage als SolrSearchRequest-Instance # options: siehe search # returns: Hash mit den SOLR-Parametern def build_solr_params solr_search_request, options config = options[:config] solr_params = {} solr_params['stats.field'] = [] fq_string = self.force_query_params options[:context] fq_string = join_params(fq_string) if fq_string.is_a?(Hash) # wegen abwärtskompatibilität (Früher lieferte die Funktion force_query_params eine Hash) solr_params[:fq] = fq_string if fq_string q = solr_search_request.build_query config q = "*:*" if q.blank? # wenn keine Query angegeben ist, dann nach allem Suchen solr_params[:q] = q # Fieldlist if q['_val_:'] # dann enthält die Query eine query-function # Der Wert dieser wird immer im field "score" abgelegt # daher dieses Feld zur Feldliste hinzufügen if options[:fieldlist] options[:fieldlist] << ',score' unless self.fieldlist[',score'] else options[:fieldlist] = '*,score' end end solr_params[:fl] = options[:fieldlist] if options.key? :fieldlist # Sortierung if solr_search_request.sorts && !solr_search_request.sorts.empty? solr_search_request.sorts.delete_if{|s| s.blank?} solr_params[:sort] = solr_search_request.sorts.map{|s| s =~ /\s(asc|desc)$/ ? s : "#{s} asc"}.join(',') end # Facets if !options[:without_facets] && solr_search_request.facets parse_facets solr_search_request, solr_params end # Stats if solr_search_request.stats_fields && !solr_search_request.stats_fields.empty? s_fields = solr_search_request.stats_fields.select{|f| !f.blank?} unless s_fields.empty? solr_params[:stats] = true solr_params['stats.field'] += s_fields end end solr_params['stats.field'].map!{|f| f.to_s}.uniq! # Gruppierung if !solr_search_request.group_field.blank? solr_params[:group] = true solr_params['group.field'] = solr_search_request.group_field solr_params['group.ngroups'] = true solr_params['group.limit'] = solr_search_request.group_size || 1 solr_params['group.truncate'] = true if solr_search_request.group_truncate end # Paginierung if options[:facets_only] solr_params[:rows] = 0 else solr_params[:rows] = solr_search_request.page_size solr_params[:start] = (solr_search_request.page-1) * solr_search_request.page_size end # Ausgabe-Format solr_params[:wt] = options[:result_format] if options.key? :result_format if MultiSolr.logger.debug? MultiSolr.logger.debug "SolrSearch#build_solr_params: #{self.inspect}\n\tSEARCH-REQUEST=#{solr_search_request.inspect}\n\t=> SOLR_PARAMS=#{solr_params.inspect}\n" end solr_params end protected # liefert wenn notwendig die Parameter als String für eine SOLR-Force-Query # sollte bei Bedarf von der nutzenden Klasse überschrieben werden. # params: # context: Hash mit weiteren Context-Informationen def force_query_params context return nil if context.nil? if context.is_a?(Hash) return join_params(context) elsif context.is_a?(String) return context else raise "Unknown fq_params=#{fq_params.inspect} ! Only Hash or Strin allowed." end end def join_params params, sep=' ' p = [] params.each{|k, v| p << "#{k}:#{v}"} return p.join(sep) end private # Bilden der Facet-Solr-Params aus den Facet-Definitionen des Requests def parse_facets solr_search_request, solr_params solr_search_request.facets.delete_if(&:blank?) # leer Elemente entfernen return if solr_search_request.facets.empty? solr_params['facet.field'] = [] solr_params['facet.range'] = [] facet_params = solr_search_request.facet_params || {} solr_search_request.facets.map(&:to_sym).each do |facet_field| field_facet_params = facet_params[facet_field] || {} stats_facet_field = field_facet_params[:stats_field] if stats_facet_field # dann Facet per Stats-Componente solr_params[:stats] = true if stats_facet_field.is_a?(Array) solr_params['stats.field'] += stats_facet_field else solr_params['stats.field'] << stats_facet_field stats_facet_field = [stats_facet_field] end stats_facet_field.each do |sfield_name| solr_params["f.#{sfield_name}.stats.facet"] ||= [] solr_params["f.#{sfield_name}.stats.facet"] << facet_field end else solr_params[:facet] = true if field_facet_params[:range] # Range-Facet solr_params[:facet] = true solr_params['facet.range'] << facet_field field_facet_params[:end] ||= 'NOW/DAY' field_facet_params[:gap] ||= '+1DAY' field_facet_params.each do |option_name, value| solr_params["f.#{facet_field}.facet.range.#{option_name}"] = value end else # normale Facet-Field solr_params['facet.field'] << facet_field field_facet_params[:mincount] ||= 1 # Facets ohne Treffer ignorieren # Nutzen der Facet-Enum-Methode fuer spezielle Felder (Felder mit kleinen Wertebereichen) if self.facet_enum_fields && self.facet_enum_fields.include?(facet_field) field_facet_params['method'] = 'enum' end field_facet_params.each do |option_name, value| solr_params["f.#{facet_field}.facet.#{option_name}"] = value end end end end if solr_params[:facet] # Umschalten der Facet-Ergebnisse in Array-Darstellung # (der json.nl-Schalter wirkt auch bei Ruby siehe http://wiki.apache.org/solr/SolJSON) solr_params['json.nl'] = 'arrarr' if solr_search_request.facet_prefix solr_params['facet.prefix'] = solr_search_request.facet_prefix end end end end