Class: Spree::SearchProvider::Meilisearch
- Defined in:
- app/models/spree/search_provider/meilisearch.rb
Constant Summary collapse
- PREFIXED_ID_PATTERN =
/\A[a-z]+_[A-Za-z0-9]+\z/- ALLOWED_STATUSES =
%w[active draft archived paused].freeze
- BUILT_IN_FILTERABLE_ATTRIBUTES =
%w[product_id status in_stock preorder store_ids channel_ids locale currency available_on discontinue_on price category_ids tags option_value_ids].freeze
- METAFIELD_RANGE_OPERATORS =
{ 'gt' => '>', 'gteq' => '>=', 'lt' => '<', 'lteq' => '<=' }.freeze
Instance Attribute Summary
Attributes inherited from Base
Class Method Summary collapse
Instance Method Summary collapse
-
#ensure_index_settings! ⇒ Object
Configure index settings for filtering, sorting, and faceting.
-
#ensure_index_settings_once! ⇒ Object
Lightweight guard — configures index settings once per provider instance.
- #filters(scope:, query: nil, filters: {}) ⇒ Object
- #index(product) ⇒ Object
- #index_batch(documents) ⇒ Object
-
#initialize(store) ⇒ Meilisearch
constructor
A new instance of Meilisearch.
- #reindex(scope = nil) ⇒ Object
- #remove(product) ⇒ Object
-
#remove_by_id(prefixed_id) ⇒ Object
Remove all documents for a product by its prefixed_id (e.g. 'prod_abc').
- #search_and_filter(scope:, query: nil, filters: {}, sort: nil, page: 1, limit: 25) ⇒ Object
Methods inherited from Base
Constructor Details
#initialize(store) ⇒ Meilisearch
Returns a new instance of Meilisearch.
15 16 17 18 19 20 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 15 def initialize(store) super require 'meilisearch' rescue LoadError raise LoadError, "Add `gem 'meilisearch'` to your Gemfile to use the Meilisearch search provider" end |
Class Method Details
.indexing_required? ⇒ Boolean
11 12 13 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 11 def self.indexing_required? true end |
Instance Method Details
#ensure_index_settings! ⇒ Object
Configure index settings for filtering, sorting, and faceting. Called automatically by reindex, but can be called separately. Waits for all settings tasks to complete before returning so that subsequent add_documents calls use the correct filterable/sortable attributes.
120 121 122 123 124 125 126 127 128 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 120 def ensure_index_settings! index = client.index(index_name) tasks = [] tasks << index.update_filterable_attributes(filterable_attributes) tasks << index.update_sortable_attributes(sortable_attributes) tasks << index.update_searchable_attributes(searchable_attributes) tasks.each { |task| task&.await } @index_settings_configured = true end |
#ensure_index_settings_once! ⇒ Object
Lightweight guard — configures index settings once per provider instance. Meilisearch settings updates are idempotent, so repeated calls are safe but we avoid the overhead by memoizing.
133 134 135 136 137 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 133 def ensure_index_settings_once! return if @index_settings_configured ensure_index_settings! end |
#filters(scope:, query: nil, filters: {}) ⇒ Object
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 50 def filters(scope:, query: nil, filters: {}) ms_result, facet_distribution = execute_search(query: query, filters: filters, sort: nil, page: 1, limit: 0, return_facets: true) unless ms_result return FiltersResult.new( filters: [], sort_options: , default_sort: 'manual', total_count: 0 ) end FiltersResult.new( filters: build_facet_response(facet_distribution), sort_options: , default_sort: 'manual', total_count: ms_result['estimatedTotalHits'] || 0 ) end |
#index(product) ⇒ Object
70 71 72 73 74 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 70 def index(product) ensure_index_settings_once! documents = presenter_class.new(product, store).call client.index(index_name).add_documents(documents, 'id') end |
#index_batch(documents) ⇒ Object
80 81 82 83 84 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 80 def index_batch(documents) return if documents.empty? client.index(index_name).add_documents(documents, 'id') end |
#reindex(scope = nil) ⇒ Object
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 94 def reindex(scope = nil) scope ||= store.products @metafield_schema = MetafieldSchema.new ensure_index_settings! indexed = 0 scope.reorder(id: :asc) .preload_associations_lazily .find_in_batches(batch_size: 500) do |batch| documents = batch.flat_map { |product| presenter_class.new(product, store).call } next if documents.empty? index_batch(documents) indexed += documents.size Rails.logger.info { "[Meilisearch] Enqueued #{documents.size} documents (#{indexed} total) for #{index_name}" } end Rails.logger.info { "[Meilisearch] Reindex complete: #{indexed} documents enqueued for #{index_name}" } indexed end |
#remove(product) ⇒ Object
76 77 78 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 76 def remove(product) remove_by_id(product.prefixed_id) end |
#remove_by_id(prefixed_id) ⇒ Object
Remove all documents for a product by its prefixed_id (e.g. 'prod_abc')
87 88 89 90 91 92 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 87 def remove_by_id(prefixed_id) filter = "product_id = '#{sanitize_prefixed_id(prefixed_id)}'" client.index(index_name).delete_documents(filter: filter) rescue ::Meilisearch::ApiError => e raise unless e.http_code == 404 end |
#search_and_filter(scope:, query: nil, filters: {}, sort: nil, page: 1, limit: 25) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 22 def search_and_filter(scope:, query: nil, filters: {}, sort: nil, page: 1, limit: 25) page = [page.to_i, 1].max limit = limit.to_i.clamp(1, 100) ms_result, _ = execute_search(query: query, filters: filters, sort: sort, page: page, limit: limit) return empty_result(scope, page, limit) unless ms_result # Hits have composite prefixed_id (prod_abc_en_USD), extract product_id (prod_abc) product_prefixed_ids = ms_result['hits'].map { |h| h['product_id'] }.uniq raw_ids = product_prefixed_ids.filter_map { |pid| Spree::Product.decode_prefixed_id(pid) } # Intersect with AR scope for security/visibility, preserving Meilisearch sort order. products = if raw_ids.any? records = scope.where(id: raw_ids).reorder(nil).index_by(&:id) raw_ids.filter_map { |id| records[id] } else scope.none end pagy = build_pagy(ms_result, page, limit) SearchResult.new( products: products, total_count: ms_result['estimatedTotalHits'] || 0, pagy: pagy ) end |