Show related packages based on description embeddings
Package pages get a "Related packages" section: a nearest neighbor query against the package's own stored embedding (no external service call), excluding the package itself. Packages without an embedding render no section.
This commit is contained in:
parent
fdf3289f54
commit
4d07f43a2e
5 changed files with 46 additions and 0 deletions
|
|
@ -39,6 +39,10 @@ Queries that share no vocabulary with the package data (gibberish,
|
||||||
keyboard mash) return an honest empty result instead of random
|
keyboard mash) return an honest empty result instead of random
|
||||||
semantic matches.
|
semantic matches.
|
||||||
|
|
||||||
|
Package pages also show semantically **related packages**, computed as
|
||||||
|
a nearest neighbor query against the package's own description
|
||||||
|
embedding - no external service call involved.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Read the doc/README.Installation.md
|
Read the doc/README.Installation.md
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ class PackagesController < ApplicationController
|
||||||
@screenshots = @package.screenshots.accessible_by(current_ability, :view).paginate(
|
@screenshots = @package.screenshots.accessible_by(current_ability, :view).paginate(
|
||||||
page: @page, per_page: 6
|
page: @page, per_page: 6
|
||||||
)
|
)
|
||||||
|
# Semantic recommendations based on the package's own description
|
||||||
|
# embedding - a plain nearest neighbor query, no service call.
|
||||||
|
@related_packages = @package.related_packages
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,16 @@ class Package < ApplicationRecord
|
||||||
text.gsub(/[\\%_]/) { |char| "\\#{char}" }
|
text.gsub(/[\\%_]/) { |char| "\\#{char}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Packages semantically similar to this one, based on the stored
|
||||||
|
# description embedding. Returns an empty relation for packages that
|
||||||
|
# have no embedding (yet).
|
||||||
|
def related_packages(limit: 6)
|
||||||
|
return Package.none unless embedding
|
||||||
|
|
||||||
|
Package.nearest_to_vector(embedding, limit: limit + 1)
|
||||||
|
.where.not(id: id).limit(limit)
|
||||||
|
end
|
||||||
|
|
||||||
# Check whether the given text shares at least one lexeme with any
|
# Check whether the given text shares at least one lexeme with any
|
||||||
# package name/description in the database. Queries without such an
|
# package name/description in the database. Queries without such an
|
||||||
# overlap (gibberish, keyboard mash, unsupported languages) can
|
# overlap (gibberish, keyboard mash, unsupported languages) can
|
||||||
|
|
@ -209,3 +219,4 @@ class Package < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/ClassLength
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,19 @@
|
||||||
/ #metadata is the anchor for this sticky container.
|
/ #metadata is the anchor for this sticky container.
|
||||||
= render(partial: 'details_rightbox', locals: {pkg: @package})
|
= render(partial: 'details_rightbox', locals: {pkg: @package})
|
||||||
|
|
||||||
|
/ Semantically similar packages, based on the package's description embedding
|
||||||
|
- if @related_packages.any?
|
||||||
|
#related
|
||||||
|
h3 Related packages
|
||||||
|
.grid-x.grid-margin-x.medium-up-2.large-up-3 data-equalizer=true
|
||||||
|
- @related_packages.each do |pkg|
|
||||||
|
.cell.pkgcard data-equalizer-watch=true
|
||||||
|
a.black href=package_path(name: pkg.name)
|
||||||
|
.text.pkgname
|
||||||
|
= pkg.name
|
||||||
|
.text.small
|
||||||
|
= pkg.description
|
||||||
|
|
||||||
javascript:
|
javascript:
|
||||||
// Where to send POST requests for uploads
|
// Where to send POST requests for uploads
|
||||||
let ajax_upload_url = '#{{upload_receive_json_path}}';
|
let ajax_upload_url = '#{{upload_receive_json_path}}';
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,21 @@ class PackagesControllerTest < ActionController::TestCase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test 'details shows semantically related packages' do
|
||||||
|
packages(:vim).update_column(:embedding, Array.new(384, 0.25))
|
||||||
|
packages(:firefox).update_column(:embedding, Array.new(384, 0.75))
|
||||||
|
|
||||||
|
get :details, params: { name: 'vim' }
|
||||||
|
assert_response :success
|
||||||
|
assert_select '#related .pkgname', text: 'firefox'
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'details without embedding renders no related section' do
|
||||||
|
get :details, params: { name: 'vim' }
|
||||||
|
assert_response :success
|
||||||
|
assert_select '#related', count: 0
|
||||||
|
end
|
||||||
|
|
||||||
test 'search uses semantic (vector) results' do
|
test 'search uses semantic (vector) results' do
|
||||||
relation = Package.where(name: packages(:vim).name)
|
relation = Package.where(name: packages(:vim).name)
|
||||||
with_stubbed_method(Package, :nearest_to_text, ->(_text, **_opts) { relation }) do
|
with_stubbed_method(Package, :nearest_to_text, ->(_text, **_opts) { relation }) do
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue