ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

如何为 Spree 编写自定义搜索 Provider 接入 Typesense 或 Algolia?

如何为 Spree 编写自定义搜索 Provider 接入 Typesense 或 Algolia? 如何为 Spree 编写自定义搜索 Provider 接入 Typesense 或 Algolia【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spree当你自带的产品搜索需要接入 Typesense、Algolia 这类外部搜索引擎而不是 Spree 内置的 Meilisearch就需要给 Spree 写一个自定义搜索 Provider。Spree 的 Store API 请求会先由控制器构建一个用于安全与可见性的 ActiveRecord scopestore.products.active(currency).accessible_by(ability)然后把搜索、过滤、排序、分面查询全部委托给 Provider。也就是说只要你的 Provider 实现了规定的方法契约前端和 API 客户端不需要任何改动——同样的q[search]、过滤参数和排序选项在任何 Provider 下都可用。适用前提你已经理解 Spree 的搜索与过滤机制。如果 Meilisearch 能满足需求不必自写 Provider按内置Meilisearch 集成文档配置即可这是文档明确给出的替代路径不是本文主路径。准备条件一个 Spree 应用Rails 环境能修改config/initializers/spree.rb搜索引擎的 Ruby 客户端例如 Typesense 需要gem typesense代码示例中 Provider 初始化时会require typesense缺 Gem 会直接报Add gem typesense to your Gemfile引擎中已准备好对应的 collection/index。Provider 基类与契约定义在 Spree::SearchProvider::Base 中indexing_required?默认返回false索引、删除、批量重建等索引方法默认是 no-op——这些都要在你的子类中覆盖。第一步创建 Provider 类类必须继承Spree::SearchProvider::Base并实现search_and_filter。以 Typesense 为例文档示例路径为app/models/my_app/search_provider/typesense.rbmodule MyApp module SearchProvider class Typesense Spree::SearchProvider::Base # Enable background indexing jobs def self.indexing_required? true end def initialize(store) super require typesense rescue LoadError raise LoadError, Add gem typesense to your Gemfile end end end endindexing_required?返回true会让SearchIndexableconcern 在产品创建、更新、销毁的after_commit时入队后台索引任务IndexJob/RemoveJob。这个 concern 挂在 Product 模型上实现见 SearchIndexable。第二步实现search_and_filter这是核心方法。它接收一个已经做好安全过滤的 base scope必须返回一个SearchResultdef 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) # 1. Query your search engine with locale/currency filtering results client.collections[index_name].documents.search({ q: query || *, query_by: name,description,sku,option_values,category_names,tags, filter_by: build_filters(filters), sort_by: build_sort(sort), page: page, per_page: limit, facet_by: in_stock,price,category_ids,option_value_ids }) # 2. Extract product IDs (documents have composite IDs, extract product_id) product_ids results[hits].map { |h| h[document][product_id] }.uniq raw_ids product_ids.filter_map { |pid| Spree::Product.decode_prefixed_id(pid) } # 3. Intersect with AR scope (safety net for authorization) products raw_ids.any? ? scope.where(id: raw_ids).reorder(nil) : scope.none # 4. Build Pagy for pagination metadata require pagy pagy Pagy::Offset.new(count: results[found], page: page, limit: limit) # 5. Return a SearchResult Spree::SearchProvider::SearchResult.new( products: products, filters: build_facet_response(results[facet_counts]), sort_options: %w[price -price name -name best_selling -available_on].map { |id| { id: id } }, default_sort: manual, total_count: results[found], pagy: pagy ) end文档中有一条明确的警告务必在搜索引擎一侧按locale、currency、store_ids、statusactive、discontinue_on过滤而不能只依赖 AR scope否则分页计数不准确。AR scope 只是授权安全网不是主过滤器。SearchResult的构造字段来自文档Spree::SearchProvider::SearchResult.new( products: ar_relation, # ActiveRecord::Relation filters: [...], # Array of facet hashes sort_options: [{ id: price }, ...], # Array of sort option objects default_sort: manual, # Default sort string total_count: 150, # Total before pagination pagy: pagy_object # Pagy::Offset or Pagy::Meilisearch )其中total_count和pagy示例数值150 等是文档示例不是固定预期。第三步实现单产品索引与删除产品按market × locale组合索引每个组合一篇文档由ProductPresenter自动处理def index(product) documents Spree::SearchProvider::ProductPresenter.new(product, store).call documents.each do |doc| client.collections[index_name].documents.upsert(doc) end end def remove(product) remove_by_id(product.prefixed_id) end def remove_by_id(prefixed_id) # Delete all locale/currency variants of this product client.collections[index_name].documents.delete( filter_by: product_id:#{prefixed_id} ) endProductPresenter#call返回数组而非单个文档。例如一个拥有 USUSD/英语和 EUEUR/德语法语市场的商店一个产品会生成 3 篇文档每篇带有扁平的name、price、locale、currency字段文档示例# [ # { prefixed_id: prod_abc_en_USD, product_id: prod_abc, locale: en, # currency: USD, name: Blue Shirt, price: 29.99, ... }, # { prefixed_id: prod_abc_de_EUR, product_id: prod_abc, locale: de, # currency: EUR, name: Blaues Hemd, price: 27.50, ... } # ]注意remove_by_id接收的是 prefixed ID如prod_abc因为调用时产品记录可能已被删除。第四步实现批量重建索引用preload_associations_lazily避免 N1 查询def reindex(scope nil) scope || store.products ensure_index_settings! scope.reorder(id: :asc) .preload_associations_lazily .find_in_batches(batch_size: 500) do |batch| documents batch.flat_map { |p| presenter_class.new(p, store).call } index_batch(documents) end end def index_batch(documents) client.collections[index_name].documents.import(documents, action: upsert) end def ensure_index_settings! # Create/update your collection schema here end文档特别指出这里必须用flat_map而不是map因为ProductPresenter#call每个产品返回的是文档数组。ensure_index_settings!是可选方法文档契约表中标记为 No用于创建或更新索引 schema会被reindex和 rake 任务调用。方法契约速查方法是否必须说明search_and_filter(scope:, query:, filters:, sort:, page:, limit:)必须搜索、过滤、排序、分页并返回 facets必须返回SearchResultself.indexing_required?必须返回true以启用后台索引任务index(product)必须索引单个产品remove(product)必须从索引删除该产品的全部 locale/currency 变体remove_by_id(prefixed_id)必须按 prefixed ID 删除产品可能已不存在reindex(scope)必须批量重建配合ensure_index_settings! 批量索引index_batch(documents)必须索引一批已序列化的文档ensure_index_settings!可选配置索引 schema第五步注册 Provider 并重建索引在config/initializers/spree.rb中按类名注册Spree.search_provider MyApp::SearchProvider::Typesense然后执行重建rake spree:search:reindex这个 rake 任务定义在 search.rake它会遍历所有 Store对每个 store 实例化 Provider 并调用provider.reindex(store.products.preload_associations_lazily)控制台会输出类似Reindexing {store_name} ({total} products) using {provider}...和完成行Done. ... documents enqueued.具体数值随你的目录变化。另外两点约束必须使用 prefixed IDctg_abc、prod_xyz、opt_abc做索引永远不要用原始数据库 ID——Spree 支持 UUID 主键。在MetafieldDefinition上标记为searchable/sortable的产品 metafield 会通过Spree::SearchProvider::MetafieldSchema投影进索引provider 与ProductPresenter共用。用 Meilisearch 时改完 definition 后要跑rake spree:search:reindexcf_*字段的文档整形依赖search_product_presenter。验证结果按以下顺序核对均为文档给出的方式预览将要索引的文档product.search_presentation返回即将发送到 provider 的文档内容可显式传 storeproduct.search_presentation(store)。这也是调试索引内容的首选手段。同步索引/删除不走后台任务product.add_to_search_index和product.remove_from_search_index适合导入数据或即时校验 provider 是否真正收到写入。重建任务输出rake spree:search:reindex正常完成并打印上述完成信息。Store API 行为由于任何 Provider 下参数不变直接请求产品列表验证curl https://api.mystore.com/api/v3/store/products?q[search]totebaglimit12 \ -H X-Spree-API-Key: pk_xxxcurl https://api.mystore.com/api/v3/store/products/filters \ -H X-Spree-API-Key: pk_xxx其中pk_xxx是文档示例中的占位值替换为你自己的 Store API key返回结构filters、sort_options、default_sort、total_count等见搜索与过滤文档。关于 Algolia 及其他引擎上面的示例代码Typesense 的query_by/facet_by/filter_by语法、client.collections调用方式是 Typesense 引擎特定的写法但 自定义搜索 Provider 指南 的适用对象明确是 Typesense, Algolia, or Elasticsearch 这类外部搜索引擎Provider 契约方法名、SearchResult结构、prefixed ID、ProductPresenter用法、注册与 reindex 流程与引擎无关。接入 Algolia 时需要把search_and_filter、index、index_batch等内部的引擎请求语法换成 Algolia 客户端的等价写法但对外接口、索引生命周期和验证步骤不变。索引生命周期小结触发点行为产品 create/update 的after_commitindexing_required?为true时入队IndexJob产品 destroy 的after_commit入队RemoveJob用prefixed_id此时记录可能已删除product.add_to_search_index同步内联索引用于批量导入或需要即时生效的场景product.search_presentation预览将要索引的文档rake spree:search:reindex全量重建所有 store 的索引后台任务机制见 SearchIndexable concern基类契约见 Base。完成 Provider 后如果后续目录规模或需求匹配内置方案也可以改回Spree.search_provider Spree::SearchProvider::MeilisearchMeilisearch 集成切换本身不需要改动任何客户端或 API 调用。【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spree创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表