Class: Typesensual::Search

Inherits:
Object
  • Object
show all
Includes:
StateHelpers
Defined in:
lib/typesensual/search.rb,
lib/typesensual/search/hit.rb,
lib/typesensual/search/facet.rb,
lib/typesensual/search/results.rb,
lib/typesensual/search/grouped_hit.rb

Defined Under Namespace

Classes: Facet, GroupedHit, Hit, Results

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(collection:, query:, query_by:) ⇒ Search

Initialize a new search object for a collection

Parameters:

  • collection (Typesensual::Collection)

    the Typesensual collection object

  • query (String)

    the query string to search for

  • query_by (String, Symbol, Array<String, Symbol>, Hash<String, Symbol>)

    the fields to search in. If a hash is provided, the keys are the fields and the values are the weights. If a string is provided, it is used directly as the query_by parameter. If an array is provided, the values are the fields.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/typesensual/search.rb', line 25

def initialize(collection:, query:, query_by:)
  @filter_by = []
  @sort_by = []
  @facet_by = []
  @facet_query = []
  # this is just used by the Results class to determine names for facets
  @facet_keys = []
  @facet_return_parent = []
  @include_fields = []
  @exclude_fields = []
  @group_by = []
  @params = {}

  @collection = collection
  @query = query

  if query_by.is_a?(Hash)
    @query_by = query_by.keys
    @query_by_weights = query_by.values
  elsif query_by.is_a?(String) || query_by.is_a?(Symbol)
    @query_by = [query_by]
  else
    @query_by = query_by
  end
end

Class Method Details

.multi(*searches) ⇒ <Typesensual::Search::Results> .multi(searches) ⇒ {Object => Typesensual::Search::Results}

Perform multiple searches in one request. There are two variants of this method, one which takes a list of anonymous queries and one which takes a hash of named queries. Named queries will probably be more readable for more than a couple of queries, but anonymous queries can be destructured directly.

Both versions accept either a Search instance or a hash of search parameters.

Overloads:



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/typesensual/search.rb', line 281

def self.multi(*searches)
  # If we have one argument and it's a hash, we're doing named searches
  if searches.count == 1 && searches.first.is_a?(Hash)
    keys = searches.first.keys
    searches = searches.first.values
  end

  results = client.multi_search.perform({
    searches: searches.flatten.map(&:query)
  })

  # Wrap our results in Result objects
  wrapped_results = results['results'].map do |result|
    exception = exception_for(result) if result['code']

    exception || Results.new(result, search: self)
  end

  # If we're doing named searches, re-key the results
  if keys
    keys.zip(wrapped_results).to_h
  else
    wrapped_results
  end
end

Instance Method Details

#exclude_fields(*fields) ⇒ self

Add fields to exclude from the search result documents

Parameters:

  • fields (String, Symbol, Array<String, Symbol>)

    the fields to exclude

Returns:

  • (self)


213
214
215
216
# File 'lib/typesensual/search.rb', line 213

def exclude_fields(*fields)
  @exclude_fields += fields.map(&:to_s)
  self
end

#facet(facets: Symbol, String, Array) ⇒ self #facet(facets: Hash) ⇒ self

Add a field to facet to the search

Overloads:

  • #facet(facets: Symbol, String, Array) ⇒ self

    Basic faceting with just a list of fields

    Examples:

    Facet by type

    # Generates `facet_by=type`
    .facet(:type)

    Facet by type and year

    # Generates `facet_by=type,year`
    .facet(['type', 'year'])

    Parameters:

    • facets (String, Symbol, Array<String, Symbol>) (defaults to: Symbol)

      the fields to facet on

  • #facet(facets: Hash) ⇒ self

    Advanced faceting with sort, ranges, return_parent, and query

    Examples:

    Facet by category, sorted by category size, and return the parent

    # Generates `facet_by=categories.id(categories.size:desc)&facet_return_parent=categories.id`
    .facet('categories.id' => { sort: { 'categories.size' => :desc }, return_parent: true })

    Facet by decade

    # Generates `facet_by=year(1990s:[1990,2000],2000s:[2000,2010])`
    .facet('year' => { ranges: { '1990s' => 1990...2000, '2000s' => [2000, 2010] })

    Parameters:

    • facets (Hash{String, Symbol => String}, Hash{String, Symbol => Hash}) (defaults to: Hash)

      the fields to facet by and their configuration. If a string is passed as the value, it is used to query the facet. If a hash is passed, each value is used to configure the facet using the following options:

      • facets[key][:sort] (String, Symbol, Hash{String, Symbol => Symbol}) — the field to sort by, and the direction to sort it in. If you pass a string or symbol, it is used as the sort direction for alphabetical sorting. Typesense only supports sorting by a single field, and passing more than one field will raise an ArgumentError.
      • facets[key][:ranges] (Hash{String, Symbol => Range, Array}) — the ranges to facet by, for numerical facets. For each key, you can pass a Range or an Array with two elements. Ranges MUST be end-exclusive and Arrays MUST have two elements, or else an ArgumentError will be raised.
      • facets[key][:return_parent] (Boolean) — if true, the parent object of the field will be returned, which can be useful for nested fields.
      • facets[key][:query] (String) — the query to facet by, equivalent to passing a string instead of a hash.

Returns:

  • (self)


132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/typesensual/search.rb', line 132

def facet(facets)
  if facets.is_a?(Hash)
    facets.each do |key, value|
      @facet_keys << key.to_s
      if value.is_a?(String)
        # Basic facet searching with a string query
        @facet_by << key.to_s
        @facet_query << "#{key}:#{value}" if value
      elsif value.is_a?(Hash)
        # Advanced faceting with sort, ranges, return_parent, and query
        facet_string = key.to_s
        facet_params = {}

        # Sort parameters
        case value[:sort]
        when Hash
          raise ArgumentError, 'Facet sort_by must have one key' if value[:sort].count != 1
          sort_key, sort_direction = value[:sort].first
          facet_params[:sort_by] = "#{sort_key}:#{sort_direction}"
        when Symbol
          facet_params[:sort_by] = "_alpha:#{value[:sort]}"
        when String
          facet_params[:sort_by] = value[:sort]
        when nil
          nil
        else
          raise ArgumentError, 'Facet sort_by must be a Hash, Symbol, or String'
        end

        # Range parameters
        if value[:ranges].is_a?(Hash)
          ranges = value[:ranges].transform_values do |range|
            case range
            when Range
              raise ArgumentError, 'Facet ranges must exclude end' unless range.exclude_end?
              "[#{range.begin},#{range.end}]"
            when Array
              raise ArgumentError, 'Facet ranges must have two elements' unless range.count == 2
              "[#{range.first},#{range.last}]"
            else
              raise ArgumentError, 'Facet ranges must be a Range or Array'
            end
          end
          facet_params.merge!(ranges)
        elsif !value[:ranges].nil?
          raise ArgumentError, 'Facet ranges must be a Hash'
        end

        # Format facet params
        unless facet_params.empty?
          facet_string += "(#{facet_params.map { |k, v| "#{k}:#{v}" }.join(',')})"
        end

        @facet_return_parent << key.to_s if value[:return_parent]
        @facet_query << "#{key}:#{value[:query]}" if value[:query]
        @facet_by << facet_string
      else
        @facet_by << key.to_s
      end
    end
  elsif facets.is_a?(Array)
    @facet_keys += facets.map(&:to_s)
    @facet_by += facets.map(&:to_s)
  else
    @facet_keys << facets.to_s
    @facet_by << facets.to_s
  end
  self
end

#filter(filter) ⇒ self

Add a filter to the search

Parameters:

  • filter (String, Symbol, Hash<String, Symbol>)

    the filter to add. If a hash is provided, the keys are the fields and the values are the values to filter by. If a string is provided, it is added directly as a filter. All filters are ANDed together.

Returns:

  • (self)


70
71
72
73
74
75
76
77
78
79
# File 'lib/typesensual/search.rb', line 70

def filter(filter)
  if filter.is_a?(Hash)
    @filter_by += filter.map { |key, value| "#{key}:#{value}" }
  elsif filter.is_a?(Array)
    @filter_by += filter.map(&:to_s)
  else
    @filter_by << filter.to_s
  end
  self
end

#group_by(*fields) ⇒ self

Add fields to group the search results by

Parameters:

  • fields (String, Symbol, Array<String, Symbol>)

    the fields to group by

Returns:

  • (self)


221
222
223
224
# File 'lib/typesensual/search.rb', line 221

def group_by(*fields)
  @group_by += fields.map(&:to_s)
  self
end

#include_fields(*fields) ⇒ self

Add fields to include in the search result documents

Parameters:

  • fields (String, Symbol, Array<String, Symbol>)

    the fields to include

Returns:

  • (self)


205
206
207
208
# File 'lib/typesensual/search.rb', line 205

def include_fields(*fields)
  @include_fields += fields.map(&:to_s)
  self
end

#loadTypesensual::Search::Results

Load the results from the search query

Returns:



255
256
257
258
259
# File 'lib/typesensual/search.rb', line 255

def load
  result = self.class.multi(self).first
  raise result if result.is_a?(StandardError)
  result
end

#page(number) ⇒ self

Set the page number to return

Parameters:

  • number (Integer)

    the page number to return

Returns:

  • (self)


61
62
63
# File 'lib/typesensual/search.rb', line 61

def page(number)
  set(page: number)
end

#per(count) ⇒ self

Set the number of results to return per page

Parameters:

  • count (Integer)

    the number of results to return per page

Returns:

  • (self)


54
55
56
# File 'lib/typesensual/search.rb', line 54

def per(count)
  set(per_page: count)
end

#queryHash

Generate the query document

Returns:

  • (Hash)

    the query document



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/typesensual/search.rb', line 236

def query
  {
    collection: @collection.name,
    filter_by: @filter_by.join(' && '),
    q: @query,
    query_by: @query_by&.join(','),
    query_by_weights: @query_by_weights&.join(','),
    sort_by: @sort_by&.join(','),
    facet_by: @facet_by&.join(','),
    facet_return_parent: @facet_return_parent&.join(','),
    facet_query: @facet_query&.join(','),
    include_fields: @include_fields&.join(','),
    exclude_fields: @exclude_fields&.join(','),
    group_by: @group_by&.join(',')
  }.merge(@params).compact
end

#set(values) ⇒ self

Set additional parameters to pass to the search

Parameters:

  • values (Hash)

    the parameters to set

Returns:

  • (self)


229
230
231
232
# File 'lib/typesensual/search.rb', line 229

def set(values)
  @params.merge!(values)
  self
end

#sort(value) ⇒ self

Add a sort to the search

Parameters:

  • value (String, Symbol, Hash<String, Symbol>)

    the sort to add to the search. If a hash is provided, the keys are the fields and the values are the directions to sort. If a string is provided, it is added directly as a sort.

Returns:

  • (self)


86
87
88
89
90
91
92
93
# File 'lib/typesensual/search.rb', line 86

def sort(value)
  if value.is_a?(Hash)
    @sort_by += value.map { |key, direction| "#{key}:#{direction}" }
  else
    @sort_by << value.to_s
  end
  self
end