Expose which search strategy produced each result
query_packages now assigns @search_sources - a hash mapping package ids to the matching strategy that put them into the results: :exact, :name_prefix, :name_contains, :semantic or :fulltext. The grid and list views render a small badge with a humanized label per result, making the search tiers observable. semantic_results() was folded into search_packages() so each branch tags its own source. Adds the rails-controller-testing gem for assigns() in tests.
This commit is contained in:
parent
61b64fae25
commit
323b7004ed
6 changed files with 69 additions and 24 deletions
|
|
@ -362,6 +362,11 @@ class PackagesController < ApplicationController
|
|||
def query_packages
|
||||
packages = Package # .order(visits: :desc)
|
||||
|
||||
# Maps package id to the search strategy that put it into the
|
||||
# result list: :exact, :name_prefix, :name_contains, :semantic or
|
||||
# :fulltext. Empty when browsing without a search term.
|
||||
@search_sources = {}
|
||||
|
||||
# text search
|
||||
if params[:search].present?
|
||||
logger.debug "Searching for #{params[:search]}"
|
||||
|
|
@ -393,15 +398,34 @@ class PackagesController < ApplicationController
|
|||
|
||||
def search_packages(query)
|
||||
exact_match = Package.find_by(name: query)
|
||||
return Package.where(id: exact_match.id) if exact_match
|
||||
if exact_match
|
||||
@search_sources[exact_match.id] = :exact
|
||||
return Package.where(id: exact_match.id)
|
||||
end
|
||||
|
||||
tokens = query.to_s.split(/\s+/)
|
||||
name_matches = (
|
||||
Package.name_starts_with(query).limit(MAX_PROMOTED_NAME_MATCHES) +
|
||||
Package.name_contains_all(tokens).limit(MAX_PROMOTED_NAME_MATCHES)
|
||||
).uniq(&:id)
|
||||
prefix_matches = Package.name_starts_with(query).limit(MAX_PROMOTED_NAME_MATCHES).to_a
|
||||
token_matches = Package.name_contains_all(query.to_s.split(/\s+/)).limit(
|
||||
MAX_PROMOTED_NAME_MATCHES
|
||||
).to_a
|
||||
prefix_matches.each { |package| @search_sources[package.id] ||= :name_prefix }
|
||||
token_matches.each { |package| @search_sources[package.id] ||= :name_contains }
|
||||
|
||||
results = (name_matches + semantic_results(query)).uniq(&:id)
|
||||
semantic = []
|
||||
if Package.lexical_overlap?(query)
|
||||
begin
|
||||
semantic = Package.nearest_to_text(query, limit: MAX_SEMANTIC_RESULTS).to_a
|
||||
semantic.each { |package| @search_sources[package.id] ||= :semantic }
|
||||
rescue Vectorizer::Error => e
|
||||
logger.error "Semantic search failed: #{e.message}"
|
||||
flash.now['error'] = 'Semantic search is unavailable right now. Showing full-text matches.'
|
||||
semantic = Package.general_search(query).limit(100).to_a
|
||||
semantic.each { |package| @search_sources[package.id] ||= :fulltext }
|
||||
end
|
||||
else
|
||||
logger.debug "Query '#{query}' has no lexical overlap with package data"
|
||||
end
|
||||
|
||||
results = (prefix_matches + token_matches + semantic).uniq(&:id)
|
||||
|
||||
# Queries that share no vocabulary with our package data can produce
|
||||
# no meaningful results. Skip the vector service round-trip and show
|
||||
|
|
@ -409,23 +433,6 @@ class PackagesController < ApplicationController
|
|||
results.empty? ? Package.none : results
|
||||
end
|
||||
|
||||
# Semantic (vector) nearest neighbor search with a full-text fallback
|
||||
# for when the external embedding service is unreachable.
|
||||
def semantic_results(query)
|
||||
unless Package.lexical_overlap?(query)
|
||||
logger.debug "Query '#{query}' has no lexical overlap with package data"
|
||||
return []
|
||||
end
|
||||
|
||||
begin
|
||||
Package.nearest_to_text(query, limit: MAX_SEMANTIC_RESULTS).to_a
|
||||
rescue Vectorizer::Error => e
|
||||
logger.error "Semantic search failed: #{e.message}"
|
||||
flash.now['error'] = 'Semantic search is unavailable right now. Showing full-text matches.'
|
||||
Package.general_search(query).limit(100).to_a
|
||||
end
|
||||
end
|
||||
|
||||
# Store a random identifier and the client's IP address in the session
|
||||
# for later identification.
|
||||
# def create_user_token
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
module PackagesHelper
|
||||
# Human readable label for the search strategy that put a package
|
||||
# into the search results (see PackagesController#search_packages)
|
||||
def search_source_label(source)
|
||||
{
|
||||
exact: 'exact match',
|
||||
name_prefix: 'name match',
|
||||
name_contains: 'name match',
|
||||
semantic: 'similar',
|
||||
fulltext: 'full-text'
|
||||
}[source]
|
||||
end
|
||||
|
||||
# Return the description and/or the version of the package
|
||||
def screenshot_caption(screenshot)
|
||||
str = []
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@
|
|||
= pkg.name
|
||||
.text
|
||||
= pkg.description
|
||||
- if @search_sources[pkg.id]
|
||||
.text.search-source
|
||||
span.label.secondary = search_source_label(@search_sources[pkg.id])
|
||||
- elsif @view_style==:list
|
||||
- @packages.to_a.each do |pkg|
|
||||
.grid-x.grid-margin-x.listview
|
||||
|
|
@ -37,6 +40,9 @@
|
|||
a href=package_path(name: pkg.name)
|
||||
=pkg.name
|
||||
p =pkg.description
|
||||
- if @search_sources[pkg.id]
|
||||
p
|
||||
span.label.secondary = search_source_label(@search_sources[pkg.id])
|
||||
.listview.longdescription
|
||||
= pkg.long_description_first_paragraph
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue