Class: SemanticSearchService
- Inherits:
-
Object
- Object
- SemanticSearchService
- Defined in:
- app/services/semantic_search_service.rb
Overview
Service for performing semantic search across all embeddable content types.
Uses pgvector for efficient vector similarity search with HNSW indexing.
Constant Summary collapse
- CONTENT_TYPE_MAPPING =
Map user-friendly type names to model class names
{ 'showcases' => 'Showcase', 'showcase' => 'Showcase', 'images' => 'Image', 'image' => 'Image', 'videos' => 'Video', 'video' => 'Video', 'posts' => 'Post', 'post' => 'Post', 'articles' => 'Article', 'article' => 'Article', 'products' => %w[Item ProductLine], 'product' => %w[Item ProductLine], 'items' => 'Item', 'item' => 'Item', 'product_lines' => 'ProductLine', 'product_line' => 'ProductLine', 'pages' => 'SiteMap', 'page' => 'SiteMap', 'reviews' => 'ReviewsIo', 'review' => 'ReviewsIo', 'publications' => 'Item', 'publication' => 'Item', 'pdfs' => 'Item', 'pdf' => 'Item', 'manuals' => 'Item', 'manual' => 'Item', 'datasheets' => 'Item', 'datasheet' => 'Item', # Article STI subtypes 'faqs' => 'ArticleFaq', 'faq' => 'ArticleFaq', 'technical' => 'ArticleTechnical', 'training' => 'ArticleTraining', 'procedures' => 'ArticleProcedure', 'procedure' => 'ArticleProcedure', # Support case data (sensitive — requires exclude_sensitive: false) 'support_notes' => 'Activity', 'support_communications' => 'Communication' }.freeze
- TYPE_DISPLAY_NAMES =
Human-readable names for result formatting
{ 'Showcase' => 'Showcase', 'Image' => 'Image', 'Video' => 'Video', 'Post' => 'Blog Post', 'Article' => 'Article', 'ProductLine' => 'Product Line', 'SiteMap' => 'Page', 'ReviewsIo' => 'Customer Review', 'Item' => 'Publication/PDF', 'Activity' => 'Support Note', 'Communication' => 'Communication' }.freeze
- PRESS_TAGS =
%w[press-release press-industry-report].freeze
Instance Attribute Summary collapse
-
#article_states ⇒ Object
readonly
Returns the value of attribute article_states.
-
#exclude_sensitive ⇒ Object
readonly
Returns the value of attribute exclude_sensitive.
-
#limit ⇒ Object
readonly
Returns the value of attribute limit.
-
#locale ⇒ Object
readonly
Returns the value of attribute locale.
-
#published_only ⇒ Object
readonly
Returns the value of attribute published_only.
-
#query ⇒ Object
readonly
Returns the value of attribute query.
-
#types ⇒ Object
readonly
Returns the value of attribute types.
Class Method Summary collapse
-
.available? ⇒ Boolean
Check if semantic search is available (has embeddings).
-
.find_all(query, types: nil, limit: 10) ⇒ Array<ApplicationRecord>
Multi-type search returning records directly.
-
.find_article_faqs(query, limit: 10) ⇒ Array<Article>
Find FAQ articles matching a query.
-
.find_article_press(query, limit: 10) ⇒ Array<Article>
Find press articles matching a query.
-
.find_article_procedures(query, limit: 10) ⇒ Array<Article>
Find procedure articles matching a query.
-
.find_article_technical(query, limit: 10) ⇒ Array<Article>
Find technical articles matching a query.
-
.find_article_training(query, limit: 10) ⇒ Array<Article>
Find training articles matching a query.
-
.find_articles(query, limit: 10) ⇒ Array<Article>
Find articles (all types) matching a query.
-
.find_images(query, limit: 10) ⇒ Array<Image>
Find images matching a query.
-
.find_pages(query, limit: 10) ⇒ Array<SiteMap>
Find pages matching a query.
-
.find_posts(query, limit: 10) ⇒ Array<Post>
Find blog posts matching a query.
-
.find_products(query, limit: 10) ⇒ Array<Item, ProductLine>
Find products (items and product lines) matching a query.
-
.find_publications(query, limit: 10) ⇒ Array<Item>
Find publications/PDFs matching a query.
-
.find_reviews(query, limit: 10) ⇒ Array<ReviewsIo>
Find customer reviews matching a query.
-
.find_showcases(query, limit: 10) ⇒ Array<Showcase>
Find showcases matching a query.
-
.find_videos(query, limit: 10) ⇒ Array<Video>
Find videos matching a query.
-
.search(query, **options) ⇒ Array<Hash>
Class-level search convenience method.
-
.stats ⇒ Hash
Get embedding statistics.
Instance Method Summary collapse
-
#initialize(query, options = {}) ⇒ SemanticSearchService
constructor
A new instance of SemanticSearchService.
-
#search(hybrid: true) ⇒ Array<Hash>
Perform semantic search.
Constructor Details
#initialize(query, options = {}) ⇒ SemanticSearchService
Returns a new instance of SemanticSearchService.
84 85 86 87 88 89 90 91 92 |
# File 'app/services/semantic_search_service.rb', line 84 def initialize(query, = {}) @query = query.to_s.strip @limit = .fetch(:limit, 10) @types = normalize_types([:types]) @locale = .fetch(:locale, 'en') @exclude_sensitive = .fetch(:exclude_sensitive, true) @published_only = .fetch(:published_only, true).to_b @article_states = Array([:article_states]).filter_map(&:presence).map(&:to_s).uniq.presence end |
Instance Attribute Details
#article_states ⇒ Object (readonly)
Returns the value of attribute article_states.
74 75 76 |
# File 'app/services/semantic_search_service.rb', line 74 def article_states @article_states end |
#exclude_sensitive ⇒ Object (readonly)
Returns the value of attribute exclude_sensitive.
74 75 76 |
# File 'app/services/semantic_search_service.rb', line 74 def exclude_sensitive @exclude_sensitive end |
#limit ⇒ Object (readonly)
Returns the value of attribute limit.
74 75 76 |
# File 'app/services/semantic_search_service.rb', line 74 def limit @limit end |
#locale ⇒ Object (readonly)
Returns the value of attribute locale.
74 75 76 |
# File 'app/services/semantic_search_service.rb', line 74 def locale @locale end |
#published_only ⇒ Object (readonly)
Returns the value of attribute published_only.
74 75 76 |
# File 'app/services/semantic_search_service.rb', line 74 def published_only @published_only end |
#query ⇒ Object (readonly)
Returns the value of attribute query.
74 75 76 |
# File 'app/services/semantic_search_service.rb', line 74 def query @query end |
#types ⇒ Object (readonly)
Returns the value of attribute types.
74 75 76 |
# File 'app/services/semantic_search_service.rb', line 74 def types @types end |
Class Method Details
.available? ⇒ Boolean
Check if semantic search is available (has embeddings)
308 309 310 |
# File 'app/services/semantic_search_service.rb', line 308 def self.available? ContentEmbedding.exists? end |
.find_all(query, types: nil, limit: 10) ⇒ Array<ApplicationRecord>
Multi-type search returning records directly
301 302 303 |
# File 'app/services/semantic_search_service.rb', line 301 def self.find_all(query, types: nil, limit: 10) new(query, types: types, limit: limit).search.pluck(:record) end |
.find_article_faqs(query, limit: 10) ⇒ Array<Article>
Find FAQ articles matching a query
244 245 246 |
# File 'app/services/semantic_search_service.rb', line 244 def self.find_article_faqs(query, limit: 10) new(query, types: ['faqs'], limit: limit).search.pluck(:record) end |
.find_article_press(query, limit: 10) ⇒ Array<Article>
Find press articles matching a query
280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'app/services/semantic_search_service.rb', line 280 def self.find_article_press(query, limit: 10) results = new(query, types: ['posts'], limit: limit * 3).search posts = results.pluck(:record).grep(Post) return [] if posts.empty? press_ids = Tagging .joins(:tag) .where(taggable_type: 'Post', taggable_id: posts.map(&:id), tags: { name: PRESS_TAGS }) .distinct .pluck(:taggable_id) .to_set posts.select { |post| press_ids.include?(post.id) }.first(limit) end |
.find_article_procedures(query, limit: 10) ⇒ Array<Article>
Find procedure articles matching a query
262 263 264 |
# File 'app/services/semantic_search_service.rb', line 262 def self.find_article_procedures(query, limit: 10) new(query, types: ['procedures'], limit: limit).search.pluck(:record) end |
.find_article_technical(query, limit: 10) ⇒ Array<Article>
Find technical articles matching a query
253 254 255 |
# File 'app/services/semantic_search_service.rb', line 253 def self.find_article_technical(query, limit: 10) new(query, types: ['technical'], limit: limit).search.pluck(:record) end |
.find_article_training(query, limit: 10) ⇒ Array<Article>
Find training articles matching a query
271 272 273 |
# File 'app/services/semantic_search_service.rb', line 271 def self.find_article_training(query, limit: 10) new(query, types: ['training'], limit: limit).search.pluck(:record) end |
.find_articles(query, limit: 10) ⇒ Array<Article>
Find articles (all types) matching a query
199 200 201 |
# File 'app/services/semantic_search_service.rb', line 199 def self.find_articles(query, limit: 10) new(query, types: %w[posts articles], limit: limit).search.pluck(:record) end |
.find_images(query, limit: 10) ⇒ Array<Image>
Find images matching a query
172 173 174 |
# File 'app/services/semantic_search_service.rb', line 172 def self.find_images(query, limit: 10) new(query, types: ['images'], limit: limit).search.pluck(:record) end |
.find_pages(query, limit: 10) ⇒ Array<SiteMap>
Find pages matching a query
217 218 219 |
# File 'app/services/semantic_search_service.rb', line 217 def self.find_pages(query, limit: 10) new(query, types: ['pages'], limit: limit).search.pluck(:record) end |
.find_posts(query, limit: 10) ⇒ Array<Post>
Find blog posts matching a query
190 191 192 |
# File 'app/services/semantic_search_service.rb', line 190 def self.find_posts(query, limit: 10) new(query, types: ['posts'], limit: limit).search.pluck(:record) end |
.find_products(query, limit: 10) ⇒ Array<Item, ProductLine>
Find products (items and product lines) matching a query
208 209 210 |
# File 'app/services/semantic_search_service.rb', line 208 def self.find_products(query, limit: 10) new(query, types: ['products'], limit: limit).search.pluck(:record) end |
.find_publications(query, limit: 10) ⇒ Array<Item>
Find publications/PDFs matching a query
235 236 237 |
# File 'app/services/semantic_search_service.rb', line 235 def self.find_publications(query, limit: 10) new(query, types: ['publications'], limit: limit).search.pluck(:record) end |
.find_reviews(query, limit: 10) ⇒ Array<ReviewsIo>
Find customer reviews matching a query
226 227 228 |
# File 'app/services/semantic_search_service.rb', line 226 def self.find_reviews(query, limit: 10) new(query, types: ['reviews'], limit: limit).search.pluck(:record) end |
.find_showcases(query, limit: 10) ⇒ Array<Showcase>
Find showcases matching a query
163 164 165 |
# File 'app/services/semantic_search_service.rb', line 163 def self.find_showcases(query, limit: 10) new(query, types: ['showcases'], limit: limit).search.pluck(:record) end |
.find_videos(query, limit: 10) ⇒ Array<Video>
Find videos matching a query
181 182 183 |
# File 'app/services/semantic_search_service.rb', line 181 def self.find_videos(query, limit: 10) new(query, types: ['videos'], limit: limit).search.pluck(:record) end |
.search(query, **options) ⇒ Array<Hash>
Class-level search convenience method
153 154 155 156 |
# File 'app/services/semantic_search_service.rb', line 153 def self.search(query, **) hybrid = .delete(:hybrid) { true } new(query, ).search(hybrid: hybrid) end |
.stats ⇒ Hash
Get embedding statistics
315 316 317 |
# File 'app/services/semantic_search_service.rb', line 315 def self.stats ContentEmbedding.group(:embeddable_type).count end |
Instance Method Details
#search(hybrid: true) ⇒ Array<Hash>
Perform semantic search
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'app/services/semantic_search_service.rb', line 108 def search(hybrid: true) return [] if query.blank? search_types, sti_type_filters = parse_types_with_filters # Gemini unified (cross-modal) space — text + images ranked together. unified_method = hybrid ? :unified_hybrid_search : :unified_search = ContentEmbedding.public_send( unified_method, query, limit: limit * 3, types: search_types, locale: locale, exclude_sensitive: exclude_sensitive, published_only: published_only, article_types: sti_type_filters.presence, article_states: article_states ) if sti_type_filters.any? = .select do |emb| next true unless emb. == 'Article' article = emb. next false unless article sti_type_filters.include?(article.class.name) end end format_results() end |