feat: search materials through beachcomber
- Material is a beachcomber Searchable (index jz_bm_materials) in the hand-written models/material_search.go; jz.bm requires summercms v0.6.4 - classes.MaterialIndexer asks Typesense for up to 250 candidates with body highlights; the app-wide Gate is on only for jz.bm-marked contexts - GET /_bm/api/v1/materials takes q: candidates are re-gated, counted and paged in SQL in rank order, each row with at most one plain snippet - search failure answers 503 Search is unavailable.; without q the list is unchanged
This commit is contained in:
@@ -6,9 +6,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/beachcomber"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
||||
"git.golem15.com/golem15/summercms/modules/surf"
|
||||
"git.golem15.com/jakub/sm-bm-plugin/classes"
|
||||
"git.golem15.com/jakub/sm-bm-plugin/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -49,23 +51,35 @@ func preloadMaterial(q *gorm.DB) *gorm.DB {
|
||||
}
|
||||
|
||||
// listMaterials returns one page of the published materials userID may
|
||||
// see, newest first.
|
||||
func listMaterials(db *gorm.DB, now time.Time, userID uint, f materialFilter, page, perPage int) ([]materialListJSON, int64, error) {
|
||||
// see. With ids nil the page is newest first. With ids non-nil (search
|
||||
// candidates in the engine's rank order) both the count and the page are
|
||||
// also limited to ids and the page follows their order; the ids never widen
|
||||
// what visibleMaterials and the filter allow. An empty ids is an empty page.
|
||||
func listMaterials(db *gorm.DB, now time.Time, userID uint, f materialFilter, ids []uint, page, perPage int) ([]materialListJSON, []models.Material, int64, error) {
|
||||
if ids != nil && len(ids) == 0 {
|
||||
return []materialListJSON{}, nil, 0, nil
|
||||
}
|
||||
q := f.apply(visibleMaterials(db.Model(&models.Material{}), now, userID))
|
||||
if ids != nil {
|
||||
q = q.Where("jz_bm_materials.id IN ?", ids)
|
||||
}
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
rowsQ := preloadMaterial(f.apply(visibleMaterials(db, now, userID)))
|
||||
if ids != nil {
|
||||
rowsQ = rowsQ.Where("jz_bm_materials.id IN ?", ids).Order(rankOrder(ids))
|
||||
} else {
|
||||
rowsQ = rowsQ.Order("jz_bm_materials.published_at DESC, jz_bm_materials.id DESC")
|
||||
}
|
||||
var rows []models.Material
|
||||
err := preloadMaterial(f.apply(visibleMaterials(db, now, userID))).
|
||||
Order("jz_bm_materials.published_at DESC, jz_bm_materials.id DESC").
|
||||
Offset((page - 1) * perPage).Limit(perPage).
|
||||
Find(&rows).Error
|
||||
err := rowsQ.Offset((page - 1) * perPage).Limit(perPage).Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
out, err := materialsJSON(db, rows)
|
||||
return out, total, err
|
||||
return out, rows, total, err
|
||||
}
|
||||
|
||||
// materialsJSON builds list rows with their file counts.
|
||||
@@ -99,31 +113,69 @@ func materialsJSON(db *gorm.DB, rows []models.Material) ([]materialListJSON, err
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Materials answers GET /materials?course=&meeting=&tag=&type=&page=&per_page=:
|
||||
// Materials answers GET /materials?q=&course=&meeting=&tag=&type=&page=&per_page=:
|
||||
// a page of the published materials the caller may see, in the {data, meta}
|
||||
// shape of lagoon.Paginate.
|
||||
// shape of lagoon.Paginate. With q (2 to 100 characters) the engine answers
|
||||
// candidates, which SQL re-gates, counts and pages in rank order, each row
|
||||
// with at most one snippet; when the engine cannot answer the request is a
|
||||
// 503. Without q the list is newest first and never calls the engine.
|
||||
func (h *Handlers) Materials(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := h.caller(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
in := query(r, "course", "meeting", "tag", "type", "page", "per_page")
|
||||
in := query(r, "q", "course", "meeting", "tag", "type", "page", "per_page")
|
||||
if !h.validate(w, r, in, materialsRules) {
|
||||
return
|
||||
}
|
||||
filter := materialFilter{CourseID: uintParam(in, "course"), MeetingID: uintParam(in, "meeting"), Tag: strings.TrimSpace(str(in, "tag")), Type: str(in, "type")}
|
||||
var cands *classes.MaterialCandidates
|
||||
if q := searchQuery(in); q != "" {
|
||||
c, err := h.MaterialIndex.Search(r.Context(), classes.MaterialSearch{Q: q, CourseID: filter.CourseID, MeetingID: filter.MeetingID, Type: filter.Type, Tag: filter.Tag})
|
||||
if err != nil {
|
||||
writeSearchUnavailable(w)
|
||||
return
|
||||
}
|
||||
cands = &c
|
||||
}
|
||||
db, ok := h.db(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
filter := materialFilter{CourseID: uintParam(in, "course"), MeetingID: uintParam(in, "meeting"), Tag: strings.TrimSpace(str(in, "tag")), Type: str(in, "type")}
|
||||
page := min(intParam(in, "page", 1), 100000)
|
||||
perPage := intParam(in, "per_page", h.perPage())
|
||||
rows, total, err := listMaterials(db, h.now(), userID, filter, page, perPage)
|
||||
var ids []uint
|
||||
if cands != nil {
|
||||
ids = cands.IDs
|
||||
if ids == nil {
|
||||
ids = []uint{}
|
||||
}
|
||||
}
|
||||
out, rows, total, err := listMaterials(db, h.now(), userID, filter, ids, page, perPage)
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
writeJSONPage(w, lagoon.Paginate(rows, page, perPage, total))
|
||||
if cands != nil {
|
||||
attachSnippets(out, rows, cands.Hits, viaMCP(r))
|
||||
}
|
||||
writeJSONPage(w, lagoon.Paginate(out, page, perPage, total))
|
||||
}
|
||||
|
||||
// attachSnippets sets the snippet of each list row from its Hit. rows are
|
||||
// the rows SQL returned (they passed the re-gate), in the order of out; the
|
||||
// Hit of an id SQL did not return is never read.
|
||||
func attachSnippets(out []materialListJSON, rows []models.Material, hits map[uint]beachcomber.Hit, mcp bool) {
|
||||
for i := range out {
|
||||
if i >= len(rows) || rows[i].ID != out[i].ID {
|
||||
continue
|
||||
}
|
||||
hit, ok := hits[rows[i].ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[i].Snippet = snippetValue(pickSnippet(hit, rows[i]), mcp)
|
||||
}
|
||||
}
|
||||
|
||||
// Material answers GET /materials/{id}: one published material the caller
|
||||
|
||||
Reference in New Issue
Block a user