about summary refs log tree commit diff stats
path: root/internal/search/search.go
blob: b7640fc4eaccbb501965060b4d4395a184c82e30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package search

import (
	"context"
	"log"
	"os"
	"path"
	"sync"

	"searchix/internal/options"

	"github.com/bcicen/jstream"
	"github.com/blevesearch/bleve/v2"
	"github.com/mitchellh/mapstructure"
	"github.com/pkg/errors"
)

const ResultsPerPage = 20

type Result[T options.NixOption] struct {
	*bleve.SearchResult
	Results []T
}

type Index[T options.NixOption] struct {
	index bleve.Index
	docs  *sync.Map
}

func New[T options.NixOption](kind string) (*Index[T], error) {
	bleve.SetLog(log.Default())
	textFieldMapping := bleve.NewTextFieldMapping()
	nameMapping := bleve.NewTextFieldMapping()
	// something special for tokenisation?
	// nameMapping.
	optionMapping := bleve.NewDocumentStaticMapping()

	optionMapping.AddFieldMappingsAt("Option", nameMapping)
	optionMapping.AddFieldMappingsAt("RelatedPackages", textFieldMapping)
	optionMapping.AddFieldMappingsAt("Description", textFieldMapping)

	indexMapping := bleve.NewIndexMapping()
	indexMapping.AddDocumentMapping("option", optionMapping)

	var err error
	index, err := bleve.NewMemOnly(indexMapping)
	// index, err = bleve.New(path.Join(cfg.DataPath, const indexFilename = "index.bleve"), indexMapping)

	if err != nil {
		return nil, errors.WithMessage(err, "error opening index")
	}
	batch := index.NewBatch()

	var docs sync.Map

	jsonFile, err := os.Open(path.Join("data", "processed", kind+".json"))
	if err != nil {
		return nil, errors.WithMessage(err, "error opening json file")
	}

	dec := jstream.NewDecoder(jsonFile, 1)
	var opt options.NixOption
	ms, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		ErrorUnused: true,
		ZeroFields:  true,
		Result:      &opt,
	})
	if err != nil {
		return nil, errors.WithMessage(err, "could not create struct decoder")
	}
	for mv := range dec.Stream() {
		opt = options.NixOption{}
		err := ms.Decode(mv.Value) // stores in opt

		if err != nil {
			return nil, errors.WithMessagef(err, "could not decode object into option, object: %#v", mv.Value)
		}

		docs.Store(opt.Option, opt)

		err = batch.Index(opt.Option, opt)
		if err != nil {
			return nil, errors.WithMessagef(err, "could not index option %s", opt.Option)
		}
	}
	err = index.Batch(batch)
	if err != nil {
		return nil, errors.WithMessage(err, "failed to run batch index operation")
	}

	return &Index[T]{
		index,
		&docs,
	}, nil
}

func (index *Index[T]) Search(ctx context.Context, keyword string, from uint64) (*Result[T], error) {
	query := bleve.NewMatchQuery(keyword)
	search := bleve.NewSearchRequest(query)
	search.Size = ResultsPerPage

	if from != 0 {
		search.From = int(from)
	}

	bleveResult, err := index.index.SearchInContext(ctx, search)
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
		if err != nil {
			return nil, errors.WithMessage(err, "failed to execute search query")
		}

		results := make([]T, min(ResultsPerPage, bleveResult.Total))
		for i, result := range bleveResult.Hits {
			doc, _ := index.docs.Load(result.ID)
			results[i] = doc.(T)
		}

		return &Result[T]{
			bleveResult,
			results,
		}, nil
	}
}