better indexing and classifier
This commit is contained in:
parent
38c2a72fe0
commit
98b53d3a33
9 changed files with 168 additions and 32 deletions
|
|
@ -14,6 +14,7 @@ type User struct {
|
||||||
type Result struct {
|
type Result struct {
|
||||||
ID string
|
ID string
|
||||||
Path string
|
Path string
|
||||||
|
Keyword string
|
||||||
HTML template.HTML
|
HTML template.HTML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,9 @@ func (db *DB) NewFile(id, path string) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) MoveFile(op, np string) error {
|
func (db *DB) MoveFile(op, np string) error {
|
||||||
|
if !db.IsIndexed(op) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
id, err := db.DeleteFile(op)
|
id, err := db.DeleteFile(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,17 @@ type Filesystem struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFilesystem(base string) (fs *Filesystem, err error) {
|
func NewFilesystem(base string) (fs *Filesystem, err error) {
|
||||||
|
if !filepath.IsAbs(base) {
|
||||||
|
err = errors.New("base dir '" + base + "' is not an absolute path")
|
||||||
|
return
|
||||||
|
}
|
||||||
fi, err := os.Stat(base)
|
fi, err := os.Stat(base)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !fi.IsDir() {
|
if !fi.IsDir() {
|
||||||
err = errors.New("base dir '" + base + "' is not a directory")
|
err = errors.New("base dir '" + base + "' is not a directory")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
fs = &Filesystem{base}
|
fs = &Filesystem{base}
|
||||||
return
|
return
|
||||||
|
|
@ -68,7 +73,7 @@ func (fs *Filesystem) ReadDir(path string) (d *Directory) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *Filesystem) Recursive(p string) (paths core.Paths) {
|
func (fs *Filesystem) Recursive(p string) (paths core.Paths) {
|
||||||
p = Clean(p)
|
p, _ = filepath.Split(Clean(p))
|
||||||
if len(p) >= 2 {
|
if len(p) >= 2 {
|
||||||
p = p[:len(p)-1]
|
p = p[:len(p)-1]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
pkg/index/classify.go
Normal file
30
pkg/index/classify.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var classes = []class{
|
||||||
|
{regexp.MustCompile(`(?i)[^e]rechnung|quittung|beleg|invoice|bill|check`), []string{"invoice", "rechnung"}},
|
||||||
|
{regexp.MustCompile(`(?i)steuer|lohn|tax|salary`), []string{"tax", "steuer"}},
|
||||||
|
{regexp.MustCompile(`(?i)vertrag|kündigung|vereinbarung|contract|termination|agreement`), []string{"contract", "vertrag"}},
|
||||||
|
{regexp.MustCompile(`(?i)versicherung|versichert|insurance|insured`), []string{"insurance", "versicherung"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
type class struct {
|
||||||
|
r *regexp.Regexp
|
||||||
|
k []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func classify(text string) (keyword []string) {
|
||||||
|
keyword = []string{"", ""}
|
||||||
|
n := 0
|
||||||
|
for _, v := range classes {
|
||||||
|
num := len(v.r.FindAllString(text, -1))
|
||||||
|
if num > n && num >= 2 {
|
||||||
|
keyword = v.k
|
||||||
|
n = num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -7,16 +7,25 @@ import (
|
||||||
"git.giftfish.de/ston1th/docstore/pkg/core"
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
"git.giftfish.de/ston1th/godrop/v2"
|
"git.giftfish.de/ston1th/godrop/v2"
|
||||||
"github.com/blevesearch/bleve"
|
"github.com/blevesearch/bleve"
|
||||||
|
"github.com/blevesearch/bleve/mapping"
|
||||||
"html/template"
|
"html/template"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxSearchResult = 101
|
const maxSearchResult = 101
|
||||||
|
|
||||||
type indexDocument struct {
|
const docType = "document"
|
||||||
|
|
||||||
|
type document struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Date string `json:"date"`
|
Keyword []string `json:"keyword"`
|
||||||
Type string `json:"type"`
|
Created time.Time `json:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *document) Type() string {
|
||||||
|
return docType
|
||||||
}
|
}
|
||||||
|
|
||||||
type Index struct {
|
type Index struct {
|
||||||
|
|
@ -30,8 +39,10 @@ func NewIndex(path string) (i *Index, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err = os.Stat(path); os.IsNotExist(err) {
|
if _, err = os.Stat(path); os.IsNotExist(err) {
|
||||||
mapping := bleve.NewIndexMapping()
|
var m *mapping.IndexMappingImpl
|
||||||
bi, err = bleve.New(path, mapping)
|
m, err = createMapping()
|
||||||
|
//m := bleve.NewIndexMapping()
|
||||||
|
bi, err = bleve.New(path, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.New("index: " + err.Error())
|
err = errors.New("index: " + err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -51,7 +62,7 @@ func (i *Index) Close() error {
|
||||||
return i.i.Close()
|
return i.i.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Index) Add(text, docType string) (id string, err error) {
|
func (i *Index) Add(text string) (id string, err error) {
|
||||||
id = newID()
|
id = newID()
|
||||||
for {
|
for {
|
||||||
doc, _ := i.i.Document(id)
|
doc, _ := i.i.Document(id)
|
||||||
|
|
@ -60,7 +71,8 @@ func (i *Index) Add(text, docType string) (id string, err error) {
|
||||||
}
|
}
|
||||||
id = newID()
|
id = newID()
|
||||||
}
|
}
|
||||||
err = i.i.Index(id, indexDocument{text, core.Now(), docType})
|
k := classify(text)
|
||||||
|
err = i.i.Index(id, &document{text, k, time.Now()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,12 +98,20 @@ func (i *Index) Search(search string) (results []core.Result, err error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for fragField, frags := range hit.Fragments {
|
for fragField, frags := range hit.Fragments {
|
||||||
if fragField == "text" || fragField == "type" || fragField == "date" {
|
if fragField == "text" || fragField == "created" {
|
||||||
for _, f := range frags {
|
for _, f := range frags {
|
||||||
r.HTML += template.HTML(f)
|
r.HTML += template.HTML(f)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if fragField == "keyword" {
|
||||||
|
var k string
|
||||||
|
for _, f := range frags {
|
||||||
|
k += f
|
||||||
|
}
|
||||||
|
r.Keyword = strings.ReplaceAll(strings.ReplaceAll(k, "<mark>", ""), "</mark>", "")
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
results = append(results, r)
|
results = append(results, r)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
pkg/index/mapping.go
Normal file
47
pkg/index/mapping.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve"
|
||||||
|
"github.com/blevesearch/bleve/analysis/analyzer/custom"
|
||||||
|
"github.com/blevesearch/bleve/analysis/token/edgengram"
|
||||||
|
"github.com/blevesearch/bleve/analysis/token/lowercase"
|
||||||
|
"github.com/blevesearch/bleve/analysis/tokenizer/unicode"
|
||||||
|
"github.com/blevesearch/bleve/mapping"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createMapping() (m *mapping.IndexMappingImpl, err error) {
|
||||||
|
m = bleve.NewIndexMapping()
|
||||||
|
|
||||||
|
err = m.AddCustomTokenFilter("bigram_tokenfilter", map[string]interface{}{
|
||||||
|
"type": edgengram.Name,
|
||||||
|
"side": edgengram.FRONT,
|
||||||
|
"min": 3.0,
|
||||||
|
"max": 25.0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = m.AddCustomAnalyzer("fulltext_ngram", map[string]interface{}{
|
||||||
|
"type": custom.Name,
|
||||||
|
"tokenizer": unicode.Name,
|
||||||
|
"token_filters": []string{
|
||||||
|
lowercase.Name,
|
||||||
|
"bigram_tokenfilter",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := bleve.NewDocumentMapping()
|
||||||
|
tm := bleve.NewTextFieldMapping()
|
||||||
|
tm.Analyzer = "fulltext_ngram"
|
||||||
|
doc.AddFieldMappingsAt("text", tm)
|
||||||
|
km := bleve.NewTextFieldMapping()
|
||||||
|
doc.AddFieldMappingsAt("keyword", km)
|
||||||
|
cm := bleve.NewDateTimeFieldMapping()
|
||||||
|
doc.AddFieldMappingsAt("created", cm)
|
||||||
|
|
||||||
|
m.AddDocumentMapping(docType, doc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -189,7 +189,7 @@ func indexHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id, err := ctx.Srv.DB.Index.Add(txt, "pdf")
|
id, err := ctx.Srv.DB.Index.Add(txt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("index: %s: %s", path, err)
|
log.Printf("index: %s: %s", path, err)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -365,7 +365,7 @@ const (
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-xs-6">
|
<div class="col">
|
||||||
<form class="form-horizontal" action="/search" method="post">
|
<form class="form-horizontal" action="/search" method="post">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -380,16 +380,31 @@ const (
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col">
|
|
||||||
{{if .Data}}
|
{{if .Data}}
|
||||||
|
<table class="table table-stripped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Keyword</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{{range $item := .Data}}
|
{{range $item := .Data}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
<a class="mono" href="{{$item.Path}}"><b>{{$item.Path}}</b></a>
|
<a class="mono" href="{{$item.Path}}"><b>{{$item.Path}}</b></a>
|
||||||
<pre class="wrap">{{$item.HTML}}</pre><br>
|
{{if $item.HTML}}
|
||||||
|
<br><pre class="wrap">{{$item.HTML}}</pre>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>{{if $item.Keyword}}<span class="badge badge-primary">{{$item.Keyword}}</span>{{end}}</td>
|
||||||
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<b>No search results found.</b>
|
<b>No search results found.</b>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{{end}}`
|
{{end}}`
|
||||||
upload = `{{define "body"}}
|
upload = `{{define "body"}}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-xs-6">
|
<div class="col">
|
||||||
<form class="form-horizontal" action="/search" method="post">
|
<form class="form-horizontal" action="/search" method="post">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -20,15 +20,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col">
|
|
||||||
{{if .Data}}
|
{{if .Data}}
|
||||||
|
<table class="table table-stripped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Keyword</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{{range $item := .Data}}
|
{{range $item := .Data}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
<a class="mono" href="{{$item.Path}}"><b>{{$item.Path}}</b></a>
|
<a class="mono" href="{{$item.Path}}"><b>{{$item.Path}}</b></a>
|
||||||
<pre class="wrap">{{$item.HTML}}</pre><br>
|
{{if $item.HTML}}
|
||||||
|
<br><pre class="wrap">{{$item.HTML}}</pre>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>{{if $item.Keyword}}<span class="badge badge-primary">{{$item.Keyword}}</span>{{end}}</td>
|
||||||
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<b>No search results found.</b>
|
<b>No search results found.</b>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue