diff --git a/dbmodels/fts5query.go b/dbmodels/fts5query.go new file mode 100644 index 0000000..e0da99c --- /dev/null +++ b/dbmodels/fts5query.go @@ -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" +} diff --git a/dbmodels/seriesses.go b/dbmodels/seriesses.go index bff2687..b3ca91a 100644 --- a/dbmodels/seriesses.go +++ b/dbmodels/seriesses.go @@ -50,13 +50,34 @@ func BasicSearchSeries(app core.App, query string) ([]*Series, []*Series, error) return nil, nil, err } - altseries, err := AltSearchSeries(app, query) + // INFO: Needing to differentiate matches + altids, err := FTS5SearchSeries(app, query) if err != nil { 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 } +// INFO: expects a normalized query string func TitleSearchSeries(app core.App, query string) ([]*Series, error) { series := []*Series{} queries := strings.Split(query, " ") @@ -65,10 +86,7 @@ func TitleSearchSeries(app core.App, query string) ([]*Series, error) { if len(queries) > 1 { for _, que := range queries[1:] { - que = strings.TrimSpace(que) - if que != "" { - q.AndWhere(dbx.Like(SERIES_TITLE_FIELD, que).Match(true, true)) - } + 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 } -func AltSearchSeries(app core.App, query string) ([]*Series, error) { - series := []*Series{} - err := app.RecordQuery(SERIES_TABLE). - Where(dbx.Like(ANNOTATION_FIELD, query).Match(true, true)). - OrderBy(SERIES_TITLE_FIELD). - All(&series) +// INFO: expects a normalized query string +// Returns all ids that match the query +func FTS5SearchSeries(app core.App, query string) ([]*FTS5IDQueryResult, error) { + seriesids := []*FTS5IDQueryResult{} + q := NewFTS5Query(). + 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 { return nil, err } - return series, nil + return seriesids, nil } func IDsForSeriesses(series []*Series) []any { @@ -296,3 +330,15 @@ func SeriesForId(app core.App, id string) (*Series, error) { 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 +} diff --git a/pages/reihen.go b/pages/reihen.go index b668da5..78b0f7c 100644 --- a/pages/reihen.go +++ b/pages/reihen.go @@ -150,8 +150,6 @@ func (p *ReihenPage) PlaceRequest(app core.App, engine *templating.Engine, e *co 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: FTS-Suche für alt. Ergebnisse func (p *ReihenPage) SearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error { diff --git a/views/routes/components/_reihe.gohtml b/views/routes/components/_reihe.gohtml index 6126915..f9e4799 100644 --- a/views/routes/components/_reihe.gohtml +++ b/views/routes/components/_reihe.gohtml @@ -1,6 +1,7 @@ {{ $model := index . 0 }} {{ $r := index . 1 }} {{ $showidseries := index . 2 }} +{{ $markar := index . 3 }} {{ $bds := index $model.relations $r.Id }} @@ -8,7 +9,9 @@