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
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
Constructor Details
#initialize(store) ⇒ Meilisearch
Returns a new instance of Meilisearch.
13 14 15 16 17 18 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 13 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
9 10 11 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 9 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.
117 118 119 120 121 122 123 124 125 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 117 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.
130 131 132 133 134 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 130 def ensure_index_settings_once! return if @index_settings_configured ensure_index_settings! end |
#filters(scope:, query: nil, filters: {}) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 48 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: .map { |id| { id: id } }, default_sort: 'manual', total_count: 0 ) end FiltersResult.new( filters: build_facet_response(facet_distribution), sort_options: .map { |id| { id: id } }, default_sort: 'manual', total_count: ms_result['estimatedTotalHits'] || 0 ) end |
#index(product) ⇒ Object
68 69 70 71 72 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 68 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
78 79 80 81 82 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 78 def index_batch(documents) return if documents.empty? client.index(index_name).add_documents(documents, 'id') end |
#reindex(scope = nil) ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 92 def reindex(scope = nil) scope ||= store.products 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
74 75 76 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 74 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’)
85 86 87 88 89 90 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 85 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
20 21 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 |
# File 'app/models/spree/search_provider/meilisearch.rb', line 20 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 |