mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2025-10-30 01:35:32 +00:00
First FTS5 search for series
This commit is contained in:
159
dbmodels/fts5query.go
Normal file
159
dbmodels/fts5query.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package dbmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FTS5IDQueryResult struct {
|
||||||
|
ID string `db:"id"`
|
||||||
|
}
|
||||||
|
type Operator int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NONE Operator = iota
|
||||||
|
OP_AND
|
||||||
|
OP_OR
|
||||||
|
OP_NOT
|
||||||
|
)
|
||||||
|
|
||||||
|
type FTS5QueryPhrase struct {
|
||||||
|
Fields []string
|
||||||
|
Op Operator
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FTS5Query struct {
|
||||||
|
FROM string
|
||||||
|
SELECT []string
|
||||||
|
MATCH []FTS5QueryPhrase
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFTS5Query() *FTS5Query {
|
||||||
|
return &FTS5Query{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *FTS5Query) From(tn string) *FTS5Query {
|
||||||
|
q.FROM = FTS5TableName(datatypes.NormalizeString(tn))
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *FTS5Query) Select(fields ...string) *FTS5Query {
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
q.SELECT = append(q.SELECT, fields...)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *FTS5Query) SelectID() *FTS5Query {
|
||||||
|
q.SELECT = append(q.SELECT, ID_FIELD)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *FTS5Query) Match(fields []string, value string) *FTS5Query {
|
||||||
|
if len(value) < 3 {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
q.MATCH = append([]FTS5QueryPhrase{
|
||||||
|
FTS5QueryPhrase{
|
||||||
|
Fields: fields,
|
||||||
|
Op: NONE,
|
||||||
|
Value: value,
|
||||||
|
}}, q.MATCH...)
|
||||||
|
|
||||||
|
if len(q.MATCH) > 1 && q.MATCH[1].Op == NONE {
|
||||||
|
q.MATCH[1].Op = OP_AND
|
||||||
|
}
|
||||||
|
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *FTS5Query) AndMatch(fields []string, value string) *FTS5Query {
|
||||||
|
if len(value) < 3 {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
q.MATCH = append(q.MATCH, FTS5QueryPhrase{
|
||||||
|
Fields: fields,
|
||||||
|
Op: OP_AND,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *FTS5Query) OrMatch(fields []string, value string) *FTS5Query {
|
||||||
|
if len(value) < 3 {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
q.MATCH = append(q.MATCH, FTS5QueryPhrase{
|
||||||
|
Fields: fields,
|
||||||
|
Op: OP_OR,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *FTS5Query) NotMatch(fields []string, value string) *FTS5Query {
|
||||||
|
if len(value) < 3 {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
q.MATCH = append(q.MATCH, FTS5QueryPhrase{
|
||||||
|
Fields: fields,
|
||||||
|
Op: OP_NOT,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *FTS5Query) Query() string {
|
||||||
|
if len(q.MATCH) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if q.FROM == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
query := q.buildQueryHead()
|
||||||
|
query += " '"
|
||||||
|
for i, m := range q.MATCH {
|
||||||
|
if i > 0 {
|
||||||
|
switch m.Op {
|
||||||
|
case OP_AND:
|
||||||
|
query += " AND"
|
||||||
|
case OP_OR:
|
||||||
|
query += " OR"
|
||||||
|
case OP_NOT:
|
||||||
|
query += " NOT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query += " { " + strings.Join(m.Fields, " ") + " } : \"" + q.Escape(m.Value) + "\""
|
||||||
|
}
|
||||||
|
|
||||||
|
query += "'"
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q FTS5Query) Escape(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "'", "''")
|
||||||
|
s = strings.ReplaceAll(s, "\"", "\"\"")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q FTS5Query) buildQueryHead() string {
|
||||||
|
if len(q.SELECT) == 0 {
|
||||||
|
return "SELECT * FROM " + q.FROM + " WHERE " + q.FROM + " MATCH"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "SELECT " + strings.Join(q.SELECT, ", ") + " FROM " + q.FROM + " WHERE " + q.FROM + " MATCH"
|
||||||
|
}
|
||||||
@@ -50,13 +50,34 @@ func BasicSearchSeries(app core.App, query string) ([]*Series, []*Series, error)
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
altseries, err := AltSearchSeries(app, query)
|
// INFO: Needing to differentiate matches
|
||||||
|
altids, err := FTS5SearchSeries(app, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// INFO: this is inefficient, but it only happens when there are matches longer than 3 characters, so we should be fine
|
||||||
|
ids := []any{}
|
||||||
|
outer_loop:
|
||||||
|
for _, id := range altids {
|
||||||
|
for _, i := range series {
|
||||||
|
sid := i.Id
|
||||||
|
if sid == id.ID {
|
||||||
|
continue outer_loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids = append(ids, id.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
altseries, err := SeriessesForIds(app, ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return series, altseries, nil
|
return series, altseries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// INFO: expects a normalized query string
|
||||||
func TitleSearchSeries(app core.App, query string) ([]*Series, error) {
|
func TitleSearchSeries(app core.App, query string) ([]*Series, error) {
|
||||||
series := []*Series{}
|
series := []*Series{}
|
||||||
queries := strings.Split(query, " ")
|
queries := strings.Split(query, " ")
|
||||||
@@ -65,10 +86,7 @@ func TitleSearchSeries(app core.App, query string) ([]*Series, error) {
|
|||||||
|
|
||||||
if len(queries) > 1 {
|
if len(queries) > 1 {
|
||||||
for _, que := range queries[1:] {
|
for _, que := range queries[1:] {
|
||||||
que = strings.TrimSpace(que)
|
q.AndWhere(dbx.Like(SERIES_TITLE_FIELD, que).Match(true, true))
|
||||||
if que != "" {
|
|
||||||
q.AndWhere(dbx.Like(SERIES_TITLE_FIELD, que).Match(true, true))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,17 +100,33 @@ func TitleSearchSeries(app core.App, query string) ([]*Series, error) {
|
|||||||
return series, nil
|
return series, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AltSearchSeries(app core.App, query string) ([]*Series, error) {
|
// INFO: expects a normalized query string
|
||||||
series := []*Series{}
|
// Returns all ids that match the query
|
||||||
err := app.RecordQuery(SERIES_TABLE).
|
func FTS5SearchSeries(app core.App, query string) ([]*FTS5IDQueryResult, error) {
|
||||||
Where(dbx.Like(ANNOTATION_FIELD, query).Match(true, true)).
|
seriesids := []*FTS5IDQueryResult{}
|
||||||
OrderBy(SERIES_TITLE_FIELD).
|
q := NewFTS5Query().
|
||||||
All(&series)
|
From(SERIES_TABLE).
|
||||||
|
SelectID()
|
||||||
|
|
||||||
|
queries := strings.Split(query, " ")
|
||||||
|
for _, que := range queries {
|
||||||
|
que := datatypes.NormalizeString(que)
|
||||||
|
if len(que) >= 3 {
|
||||||
|
q.AndMatch([]string{SERIES_TITLE_FIELD, ANNOTATION_FIELD, REFERENCES_FIELD}, que)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
querystring := q.Query()
|
||||||
|
if querystring == "" {
|
||||||
|
return seriesids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := app.DB().NewQuery(querystring).All(&seriesids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return series, nil
|
return seriesids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IDsForSeriesses(series []*Series) []any {
|
func IDsForSeriesses(series []*Series) []any {
|
||||||
@@ -296,3 +330,15 @@ func SeriesForId(app core.App, id string) (*Series, error) {
|
|||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SeriessesForIds(app core.App, ids []any) ([]*Series, error) {
|
||||||
|
series := []*Series{}
|
||||||
|
err := app.RecordQuery(SERIES_TABLE).
|
||||||
|
Where(dbx.HashExp{ID_FIELD: ids}).
|
||||||
|
All(&series)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return series, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,8 +150,6 @@ func (p *ReihenPage) PlaceRequest(app core.App, engine *templating.Engine, e *co
|
|||||||
return p.Get(e, engine, data)
|
return p.Get(e, engine, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUG: alternative treffer haben keine relations
|
|
||||||
// TODO: Suche nach Musenalm-ID
|
|
||||||
// TODO: Suchverhalten bei gefilterten Personen, Orten und Jahren
|
// TODO: Suchverhalten bei gefilterten Personen, Orten und Jahren
|
||||||
// TODO: FTS-Suche für alt. Ergebnisse
|
// TODO: FTS-Suche für alt. Ergebnisse
|
||||||
func (p *ReihenPage) SearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
func (p *ReihenPage) SearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{{ $model := index . 0 }}
|
{{ $model := index . 0 }}
|
||||||
{{ $r := index . 1 }}
|
{{ $r := index . 1 }}
|
||||||
{{ $showidseries := index . 2 }}
|
{{ $showidseries := index . 2 }}
|
||||||
|
{{ $markar := index . 3 }}
|
||||||
|
|
||||||
{{ $bds := index $model.relations $r.Id }}
|
{{ $bds := index $model.relations $r.Id }}
|
||||||
|
|
||||||
@@ -8,7 +9,9 @@
|
|||||||
<div class="flex flex-row mb-1.5">
|
<div class="flex flex-row mb-1.5">
|
||||||
<div class="grow-0 shrink-0 w-[12rem] flex flex-col">
|
<div class="grow-0 shrink-0 w-[12rem] flex flex-col">
|
||||||
{{ if $r.References }}
|
{{ if $r.References }}
|
||||||
<div class="text-sm font-sans px-2 py-1 bg-stone-100">{{ $r.References }}</div>
|
<div class="text-sm font-sans px-2 py-1 bg-stone-100 {{ if $markar }}reihen-text{{ end }}">
|
||||||
|
{{ $r.References }}
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if $showidseries }}
|
{{ if $showidseries }}
|
||||||
{{ range $_, $rel := $bds }}
|
{{ range $_, $rel := $bds }}
|
||||||
@@ -29,11 +32,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grow-0 ml-8 -indent-3">
|
<div class="grow-0 ml-8 -indent-3">
|
||||||
<div class="reihen-text contents">
|
<div class="contents">
|
||||||
<span class="font-bold">{{ $r.Title }}</span>
|
<span class="font-bold reihen-text">{{ $r.Title }}</span>
|
||||||
{{ if $r.Annotation }}
|
{{ if $r.Annotation }}
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<span class="">{{ Safe $r.Annotation }}</span>
|
<span class="{{ if $markar }}reihen-text{{ end }}">{{ Safe $r.Annotation }}</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ let mark_instance = new Mark(elements);
|
|||||||
{{ if and .search .idseries }}
|
{{ if and .search .idseries }}
|
||||||
<div class="mb-1 max-w-[60rem] hyphens-auto">
|
<div class="mb-1 max-w-[60rem] hyphens-auto">
|
||||||
{{ range $id, $r := .idseries }}
|
{{ range $id, $r := .idseries }}
|
||||||
{{ template "_reihe" (Arr $model $r true) }}
|
{{ template "_reihe" (Arr $model $r true false) }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -39,7 +39,7 @@ let mark_instance = new Mark(elements);
|
|||||||
{{ if .series }}
|
{{ if .series }}
|
||||||
<div class="mb-1 max-w-[60rem] hyphens-auto">
|
<div class="mb-1 max-w-[60rem] hyphens-auto">
|
||||||
{{ range $id, $r := .series }}
|
{{ range $id, $r := .series }}
|
||||||
{{ template "_reihe" (Arr $model $r false) }}
|
{{ template "_reihe" (Arr $model $r false false) }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -57,11 +57,11 @@ let mark_instance = new Mark(elements);
|
|||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="border-t mb-1.5 text-sm font-sans text-right pt-0.5">
|
<div class="border-t mb-1.5 text-sm font-sans text-right pt-0.5">
|
||||||
Treffer in Anmerkungen, Verweisen etc. ↓
|
Treffer in allen Feldern (Anmerkungen, Verweisen etc.) ↓
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1 max-w-[60rem] hyphens-auto">
|
<div class="mb-1 max-w-[60rem] hyphens-auto">
|
||||||
{{ range $id, $r := .altseries }}
|
{{ range $id, $r := .altseries }}
|
||||||
{{ template "_reihe" (Arr $model $r false) }}
|
{{ template "_reihe" (Arr $model $r false true) }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
Reference in New Issue
Block a user