begin require 'time' require 'date' require 'rsolr' rescue LoadError require 'rubygems' require 'rsolr' end require File.join(File.dirname(__FILE__), 'light_config') %w(util adapters configuration setup composite_setup field field_factory data_extractor indexer query search facet facet_row instantiated_facet instantiated_facet_row date_facet date_facet_row query_facet query_facet_row session type dsl).each do |filename| require File.join(File.dirname(__FILE__), 'sunspot', filename) end # # The Sunspot module provides class-method entry points to most of the # functionality provided by the Sunspot library. Internally, the Sunspot # singleton class contains a (non-thread-safe!) instance of Sunspot::Session, # to which it delegates most of the class methods it exposes. In the method # documentation below, this instance is referred to as the "singleton session". # # Though the singleton session provides a convenient entry point to Sunspot, # it is by no means required to use the Sunspot class methods. Multiple sessions # may be instantiated and used (if you need to connect to multiple Solr # instances, for example.) # # Note that the configuration of classes for index/search (the +setup+ # method) is _not_ session-specific, but rather global. # module Sunspot UnrecognizedFieldError = Class.new(Exception) UnrecognizedRestrictionError = Class.new(Exception) NoAdapterError = Class.new(Exception) NoSetupError = Class.new(Exception) class <:: class to configure # # ==== Example # # Sunspot.setup(Post) do # text :title, :body # string :author_name # integer :blog_id # integer :category_ids # float :average_rating, :using => :ratings_average # time :published_at # string :sort_title do # title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title # end # end # # ====== Attribute Fields vs. Virtual Fields # # Attribute fields call a method on the indexed object and index the # return value. All of the fields defined above except for the last one are # attribute fields. By default, the field name will also be the attribute # used; this can be overriden with the +:using+ option, as in # +:average_rating+ above. In that case, the attribute +:ratings_average+ # will be indexed with the field name +:average_rating+. # # +:sort_title+ is a virtual field, which evaluates the block inside the # context of the instance being indexed, and indexes the value returned # by the block. If the block you pass takes an argument, it will be passed # the instance rather than being evaluated inside of it; so, the following # example is equivalent to the one above (assuming #title is public): # # Sunspot.setup(Post) do # string :sort_title do |post| # post.title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title # end # end # # ===== Field Types # # The available types are: # # * +text+ # * +string+ # * +integer+ # * +float+ # * +time+ # * +boolean+ # # Note that the +text+ type behaves quite differently from the others - # this is the type that is indexed as fulltext, and is searched using the # +keywords+ method inside the search DSL. Text fields cannot have # restrictions set on them, nor can they be used in order statements or # for facets. All other types are indexed literally, and thus can be used # for all of those operations. They will not, however, be searched in # fulltext. In this way, Sunspot provides a complete barrier between # fulltext fields and value fields. # # It is fine to specify a field both as a text field and a string field; # internally, the fields will have different names so there is no danger # of conflict. # # ===== Dynamic Fields # # For use cases which have highly dynamic data models (for instance, an # open set of key-value pairs attached to a model), it may be useful to # defer definition of fields until indexing time. Sunspot exposes dynamic # fields, which define a data accessor (either attribute or virtual, see # above), which accepts a hash of field names to values. Note that the field # names in the hash are internally scoped to the base name of the dynamic # field, so any time they are referred to, they are referred to using both # the base name and the dynamic (runtime-specified) name. # # Dynamic fields are speficied in the setup block using the type name # prefixed by +dynamic_+. For example: # # Sunspot.setup(Post) do # dynamic_string :custom_values do # key_value_pairs.inject({}) do |hash, key_value_pair| # hash[key_value_pair.key.to_sym] = key_value_pair.value # end # end # end # # If you later wanted to facet all of the values for the key "cuisine", # you could issue: # # Sunspot.search(Post) do # dynamic :custom_values do # facet :cuisine # end # end # # In the documentation, +:custom_values+ is referred to as the "base name" - # that is, the one specified statically - and +:cuisine+ is referred to as # the dynamic name, which is the part that is specified at indexing time. # def setup(clazz, &block) Setup.setup(clazz, &block) end # Indexes objects on the singleton session. # # ==== Parameters # # objects...:: objects to index (may pass an array or varargs) # # ==== Example # # post1, post2 = Array(2) { Post.create } # Sunspot.index(post1, post2) # # Note that indexed objects won't be reflected in search until a commit is # sent - see Sunspot.index! and Sunspot.commit # def index(*objects) session.index(*objects) end # Indexes objects on the singleton session and commits immediately. # # See: Sunspot.index and Sunspot.commit # # ==== Parameters # # objects...:: objects to index (may pass an array or varargs) # def index!(*objects) session.index!(*objects) end # Commits the singleton session # # When documents are added to or removed from Solr, the changes are # initially stored in memory, and are not reflected in Solr's existing # searcher instance. When a commit message is sent, the changes are written # to disk, and a new searcher is spawned. Commits are thus fairly # expensive, so if your application needs to index several documents as part # of a single operation, it is advisable to index them all and then call # commit at the end of the operation. # # Note that Solr can also be configured to automatically perform a commit # after either a specified interval after the last change, or after a # specified number of documents are added. See # http://wiki.apache.org/solr/SolrConfigXml # def commit session.commit end # # Create a new Search instance, but do not execute it immediately. Generally # you will want to use the #search method to execute searches using the # DSL; however, if you are building searches dynamically (using the Builder # pattern, for instance), it may be easier to access the Query API directly. # # ==== Parameters # # types...:: # Zero, one, or more types to search for. If no types are passed, all # configured types will be searched for. # # ==== Returns # # Sunspot::Search:: # Search object, not yet executed. Query parameters can be added manually; # then #execute! should be called. # def new_search(*types) session.new_search(*types) end # Search for objects in the index. # # ==== Parameters # # types...:: # Zero, one, or more types to search for. If no types are passed, all # configured types will be searched. # # ==== Options (last argument, optional) # # :keywords:: Fulltext search string # :conditions:: # Hash of key-value pairs to be used as restrictions. Keys are field # names. Scalar values are used as equality restrictions; arrays are used # as "any of" restrictions; and Ranges are used as range restrictions. # :order:: order field and direction (e.g., 'updated_at desc') # :page:: Page to start on for pagination # :per_page:: # Number of results to use per page. Ignored if :page is not specified. # # ==== Returns # # Sunspot::Search:: Object containing results, facets, count, etc. # # The fields available for restriction, ordering, etc. are those that meet # the following criteria: # # * They are not of type +text+. # * They are defined for all of the classes being searched # * They have the same data type for all of the classes being searched # * They have the same multiple flag for all of the classes being searched. # # The restrictions available are the constants defined in the # Sunspot::Restriction class. The standard restrictions are: # # with(:field_name).equal_to(value) # with(:field_name, value) # shorthand for above # with(:field_name).less_than(value) # with(:field_name).greater_than(value) # with(:field_name).between(value1..value2) # with(:field_name).any_of([value1, value2, value3]) # with(:field_name).all_of([value1, value2, value3]) # without(some_instance) # exclude that particular instance # # +without+ can be substituted for +with+, causing the restriction to be # negated. In the last example above, only +without+ works, as it does not # make sense to search only for an instance you already have. # # Equality restrictions can take +nil+ as a value, which restricts the # results to documents that have no value for the given field. Passing +nil+ # as a value to other restriction types is illegal. Thus: # # with(:field_name, nil) # ok # with(:field_name).equal_to(nil) # ok # with(:field_name).less_than(nil) # bad # # ==== Example # # Sunspot.search(Post) do # keywords 'great pizza' # with(:published_at).less_than Time.now # with :blog_id, 1 # without current_post # facet :category_ids # order_by :published_at, :desc # paginate 2, 15 # end # # If the block passed to #search takes an argument, that argument will # present the DSL, and the block will be evaluated in the calling context. # This will come in handy for building searches using instance data or # methods, e.g.: # # Sunspot.search(Post) do |query| # query.with(:blog_id, @current_blog.id) # end # # See Sunspot::DSL::Search, Sunspot::DSL::Scope, Sunspot::DSL::FieldQuery # and Sunspot::DSL::Query for the full API presented inside the block. # def search(*types, &block) session.search(*types, &block) end # Remove objects from the index. Any time an object is destroyed, it must # be removed from the index; otherwise, the index will contain broken # references to objects that do not exist, which will cause errors when # those objects are matched in search results. # # ==== Parameters # # objects...:: # Objects to remove from the index (may pass an array or varargs) # # ==== Example # # post.destroy # Sunspot.remove(post) # def remove(*objects) session.remove(*objects) end # # Remove objects from the index and immediately commit. See Sunspot.remove # # ==== Parameters # # objects...:: Objects to remove from the index # def remove! session.remove!(*objects) end # # Remove an object from the index using its class name and primary key. # Useful if you know this information and want to remove an object without # instantiating it from persistent storage # # ==== Parameters # # clazz:: Class of the object, or class name as a string or symbol # id:: # Primary key of the object. This should be the same id that would be # returned by the class's instance adapter. # def remove_by_id(clazz, id) session.remove_by_id(clazz, id) end # # Remove an object by class name and primary key, and immediately commit. # See #remove_by_id and #commit # def remove_by_id!(clazz, id) session.remove_by_id!(clazz, id) end # Remove all objects of the given classes from the index. There isn't much # use for this in general operations but it can be useful for maintenance, # testing, etc. If no arguments are passed, remove everything from the # index. # # ==== Parameters # # classes...:: # classes for which to remove all instances from the index (may pass an # array or varargs) # # ==== Example # # Sunspot.remove_all(Post, Blog) # def remove_all(*classes) session.remove_all(*classes) end # # Remove all objects of the given classes from the index and immediately # commit. See Sunspot.remove_all # # ==== Parameters # # classes...:: # classes for which to remove all instances from the index def remove_all!(*classes) session.remove_all!(*classes) end # # Process all adds in a batch. Any Sunspot adds initiated inside the block # will be sent in bulk when the block finishes. Useful if your application # initiates index adds from various places in code as part of a single # operation; doing a batch add will give better performance. # # ==== Example # # Sunspot.batch do # post = Post.new # Sunspot.add(post) # comment = Comment.new # Sunspot.add(comment) # end # # Sunspot will send both the post and the comment in a single request. # def batch(&block) session.batch(&block) end # # True if documents have been added, updated, or removed since the last # commit. # # ==== Returns # # Boolean:: Whether there have been any updates since the last commit # def dirty? session.dirty? end # # Sends a commit if the session is dirty (see #dirty?). # def commit_if_dirty session.commit_if_dirty end # Returns the configuration associated with the singleton session. See # Sunspot::Configuration for details. # # ==== Returns # # LightConfig::Configuration:: configuration for singleton session # def config session.config end # # Resets the singleton session. This is useful for clearing out all # static data between tests, but probably nowhere else. # # ==== Parameters # # keep_config:: # Whether to retain the configuration used by the current singleton # session. Default false. # def reset!(keep_config = false) config = if keep_config session.config else Configuration.build end @session = Session.new(config) end private # # Get the singleton session, creating it if none yet exists. # # ==== Returns # # Sunspot::Session:: the singleton session # def session #:nodoc: @session ||= Session.new end end end