Search bar for issues/pulls (#530)
parent
8bc431952f
commit
833f8b94c2
|
@ -41,5 +41,6 @@ coverage.out
|
||||||
/dist
|
/dist
|
||||||
/custom
|
/custom
|
||||||
/data
|
/data
|
||||||
|
/indexers
|
||||||
/log
|
/log
|
||||||
/public/img/avatar
|
/public/img/avatar
|
||||||
|
|
|
@ -158,6 +158,10 @@ SSL_MODE = disable
|
||||||
; For "sqlite3" and "tidb", use absolute path when you start as service
|
; For "sqlite3" and "tidb", use absolute path when you start as service
|
||||||
PATH = data/gitea.db
|
PATH = data/gitea.db
|
||||||
|
|
||||||
|
[indexer]
|
||||||
|
ISSUE_INDEXER_PATH = indexers/issues.bleve
|
||||||
|
UPDATE_BUFFER_LEN = 20
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
|
|
||||||
[security]
|
[security]
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -451,8 +452,11 @@ func (issue *Issue) ReadBy(userID int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
|
func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
|
||||||
_, err := e.Id(issue.ID).Cols(cols...).Update(issue)
|
if _, err := e.Id(issue.ID).Cols(cols...).Update(issue); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
UpdateIssueIndexer(issue)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateIssueCols only updates values of specific columns for given issue.
|
// UpdateIssueCols only updates values of specific columns for given issue.
|
||||||
|
@ -733,6 +737,8 @@ func newIssue(e *xorm.Session, opts NewIssueOptions) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateIssueIndexer(opts.Issue)
|
||||||
|
|
||||||
if len(opts.Attachments) > 0 {
|
if len(opts.Attachments) > 0 {
|
||||||
attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
|
attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -865,10 +871,11 @@ type IssuesOptions struct {
|
||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
RepoIDs []int64
|
RepoIDs []int64
|
||||||
Page int
|
Page int
|
||||||
IsClosed bool
|
IsClosed util.OptionalBool
|
||||||
IsPull bool
|
IsPull util.OptionalBool
|
||||||
Labels string
|
Labels string
|
||||||
SortType string
|
SortType string
|
||||||
|
IssueIDs []int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// sortIssuesSession sort an issues-related session based on the provided
|
// sortIssuesSession sort an issues-related session based on the provided
|
||||||
|
@ -894,11 +901,23 @@ func sortIssuesSession(sess *xorm.Session, sortType string) {
|
||||||
|
|
||||||
// Issues returns a list of issues by given conditions.
|
// Issues returns a list of issues by given conditions.
|
||||||
func Issues(opts *IssuesOptions) ([]*Issue, error) {
|
func Issues(opts *IssuesOptions) ([]*Issue, error) {
|
||||||
if opts.Page <= 0 {
|
var sess *xorm.Session
|
||||||
opts.Page = 1
|
if opts.Page >= 0 {
|
||||||
|
var start int
|
||||||
|
if opts.Page == 0 {
|
||||||
|
start = 0
|
||||||
|
} else {
|
||||||
|
start = (opts.Page - 1) * setting.UI.IssuePagingNum
|
||||||
|
}
|
||||||
|
sess = x.Limit(setting.UI.IssuePagingNum, start)
|
||||||
|
} else {
|
||||||
|
sess = x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := x.Limit(setting.UI.IssuePagingNum, (opts.Page-1)*setting.UI.IssuePagingNum)
|
if len(opts.IssueIDs) > 0 {
|
||||||
|
sess.In("issue.id", opts.IssueIDs)
|
||||||
|
}
|
||||||
|
|
||||||
if opts.RepoID > 0 {
|
if opts.RepoID > 0 {
|
||||||
sess.And("issue.repo_id=?", opts.RepoID)
|
sess.And("issue.repo_id=?", opts.RepoID)
|
||||||
|
@ -906,7 +925,13 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
|
||||||
// In case repository IDs are provided but actually no repository has issue.
|
// In case repository IDs are provided but actually no repository has issue.
|
||||||
sess.In("issue.repo_id", opts.RepoIDs)
|
sess.In("issue.repo_id", opts.RepoIDs)
|
||||||
}
|
}
|
||||||
sess.And("issue.is_closed=?", opts.IsClosed)
|
|
||||||
|
switch opts.IsClosed {
|
||||||
|
case util.OptionalBoolTrue:
|
||||||
|
sess.And("issue.is_closed=true")
|
||||||
|
case util.OptionalBoolFalse:
|
||||||
|
sess.And("issue.is_closed=false")
|
||||||
|
}
|
||||||
|
|
||||||
if opts.AssigneeID > 0 {
|
if opts.AssigneeID > 0 {
|
||||||
sess.And("issue.assignee_id=?", opts.AssigneeID)
|
sess.And("issue.assignee_id=?", opts.AssigneeID)
|
||||||
|
@ -926,7 +951,12 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
|
||||||
sess.And("issue.milestone_id=?", opts.MilestoneID)
|
sess.And("issue.milestone_id=?", opts.MilestoneID)
|
||||||
}
|
}
|
||||||
|
|
||||||
sess.And("issue.is_pull=?", opts.IsPull)
|
switch opts.IsPull {
|
||||||
|
case util.OptionalBoolTrue:
|
||||||
|
sess.And("issue.is_pull=true")
|
||||||
|
case util.OptionalBoolFalse:
|
||||||
|
sess.And("issue.is_pull=false")
|
||||||
|
}
|
||||||
|
|
||||||
sortIssuesSession(sess, opts.SortType)
|
sortIssuesSession(sess, opts.SortType)
|
||||||
|
|
||||||
|
@ -1168,10 +1198,11 @@ type IssueStatsOptions struct {
|
||||||
MentionedID int64
|
MentionedID int64
|
||||||
PosterID int64
|
PosterID int64
|
||||||
IsPull bool
|
IsPull bool
|
||||||
|
IssueIDs []int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssueStats returns issue statistic information by given conditions.
|
// GetIssueStats returns issue statistic information by given conditions.
|
||||||
func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
|
func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
|
||||||
stats := &IssueStats{}
|
stats := &IssueStats{}
|
||||||
|
|
||||||
countSession := func(opts *IssueStatsOptions) *xorm.Session {
|
countSession := func(opts *IssueStatsOptions) *xorm.Session {
|
||||||
|
@ -1179,6 +1210,10 @@ func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
|
||||||
Where("issue.repo_id = ?", opts.RepoID).
|
Where("issue.repo_id = ?", opts.RepoID).
|
||||||
And("is_pull = ?", opts.IsPull)
|
And("is_pull = ?", opts.IsPull)
|
||||||
|
|
||||||
|
if len(opts.IssueIDs) > 0 {
|
||||||
|
sess.In("issue.id", opts.IssueIDs)
|
||||||
|
}
|
||||||
|
|
||||||
if len(opts.Labels) > 0 && opts.Labels != "0" {
|
if len(opts.Labels) > 0 && opts.Labels != "0" {
|
||||||
labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ","))
|
labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1210,13 +1245,20 @@ func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
|
||||||
return sess
|
return sess
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.OpenCount, _ = countSession(opts).
|
var err error
|
||||||
|
stats.OpenCount, err = countSession(opts).
|
||||||
And("is_closed = ?", false).
|
And("is_closed = ?", false).
|
||||||
Count(&Issue{})
|
Count(&Issue{})
|
||||||
stats.ClosedCount, _ = countSession(opts).
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats.ClosedCount, err = countSession(opts).
|
||||||
And("is_closed = ?", true).
|
And("is_closed = ?", true).
|
||||||
Count(&Issue{})
|
Count(&Issue{})
|
||||||
return stats
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
|
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
|
||||||
|
@ -1294,7 +1336,11 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
|
||||||
|
|
||||||
func updateIssue(e Engine, issue *Issue) error {
|
func updateIssue(e Engine, issue *Issue) error {
|
||||||
_, err := e.Id(issue.ID).AllCols().Update(issue)
|
_, err := e.Id(issue.ID).AllCols().Update(issue)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
UpdateIssueIndexer(issue)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateIssue updates all fields of given issue.
|
// UpdateIssue updates all fields of given issue.
|
||||||
|
|
|
@ -454,28 +454,20 @@ func UpdateComment(c *Comment) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCommentByID deletes the comment by given ID.
|
// DeleteComment deletes the comment
|
||||||
func DeleteCommentByID(id int64) error {
|
func DeleteComment(comment *Comment) error {
|
||||||
comment, err := GetCommentByID(id)
|
|
||||||
if err != nil {
|
|
||||||
if IsErrCommentNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sess := x.NewSession()
|
sess := x.NewSession()
|
||||||
defer sessionRelease(sess)
|
defer sessionRelease(sess)
|
||||||
if err = sess.Begin(); err != nil {
|
if err := sess.Begin(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = sess.Id(comment.ID).Delete(new(Comment)); err != nil {
|
if _, err := sess.Id(comment.ID).Delete(new(Comment)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment.Type == CommentTypeComment {
|
if comment.Type == CommentTypeComment {
|
||||||
if _, err = sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
|
if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"github.com/blevesearch/bleve"
|
||||||
|
"github.com/blevesearch/bleve/analysis/analyzer/simple"
|
||||||
|
"github.com/blevesearch/bleve/search/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
// issueIndexerUpdateQueue queue of issues that need to be updated in the issues
|
||||||
|
// indexer
|
||||||
|
var issueIndexerUpdateQueue chan *Issue
|
||||||
|
|
||||||
|
// issueIndexer (thread-safe) index for searching issues
|
||||||
|
var issueIndexer bleve.Index
|
||||||
|
|
||||||
|
// issueIndexerData data stored in the issue indexer
|
||||||
|
type issueIndexerData struct {
|
||||||
|
ID int64
|
||||||
|
RepoID int64
|
||||||
|
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// numericQuery an numeric-equality query for the given value and field
|
||||||
|
func numericQuery(value int64, field string) *query.NumericRangeQuery {
|
||||||
|
f := float64(value)
|
||||||
|
tru := true
|
||||||
|
q := bleve.NewNumericRangeInclusiveQuery(&f, &f, &tru, &tru)
|
||||||
|
q.SetField(field)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchIssuesByKeyword searches for issues by given conditions.
|
||||||
|
// Returns the matching issue IDs
|
||||||
|
func SearchIssuesByKeyword(repoID int64, keyword string) ([]int64, error) {
|
||||||
|
fields := strings.Fields(strings.ToLower(keyword))
|
||||||
|
indexerQuery := bleve.NewConjunctionQuery(
|
||||||
|
numericQuery(repoID, "RepoID"),
|
||||||
|
bleve.NewDisjunctionQuery(
|
||||||
|
bleve.NewPhraseQuery(fields, "Title"),
|
||||||
|
bleve.NewPhraseQuery(fields, "Content"),
|
||||||
|
))
|
||||||
|
search := bleve.NewSearchRequestOptions(indexerQuery, 2147483647, 0, false)
|
||||||
|
search.Fields = []string{"ID"}
|
||||||
|
|
||||||
|
result, err := issueIndexer.Search(search)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
issueIDs := make([]int64, len(result.Hits))
|
||||||
|
for i, hit := range result.Hits {
|
||||||
|
issueIDs[i] = int64(hit.Fields["ID"].(float64))
|
||||||
|
}
|
||||||
|
return issueIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitIssueIndexer initialize issue indexer
|
||||||
|
func InitIssueIndexer() {
|
||||||
|
_, err := os.Stat(setting.Indexer.IssuePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if err = createIssueIndexer(); err != nil {
|
||||||
|
log.Fatal(4, "CreateIssuesIndexer: %v", err)
|
||||||
|
}
|
||||||
|
if err = populateIssueIndexer(); err != nil {
|
||||||
|
log.Fatal(4, "PopulateIssuesIndex: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Fatal(4, "InitIssuesIndexer: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
issueIndexer, err = bleve.Open(setting.Indexer.IssuePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(4, "InitIssuesIndexer, open index: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
issueIndexerUpdateQueue = make(chan *Issue, setting.Indexer.UpdateQueueLength)
|
||||||
|
go processIssueIndexerUpdateQueue()
|
||||||
|
// TODO close issueIndexer when Gitea closes
|
||||||
|
}
|
||||||
|
|
||||||
|
// createIssueIndexer create an issue indexer if one does not already exist
|
||||||
|
func createIssueIndexer() error {
|
||||||
|
mapping := bleve.NewIndexMapping()
|
||||||
|
docMapping := bleve.NewDocumentMapping()
|
||||||
|
|
||||||
|
docMapping.AddFieldMappingsAt("ID", bleve.NewNumericFieldMapping())
|
||||||
|
docMapping.AddFieldMappingsAt("RepoID", bleve.NewNumericFieldMapping())
|
||||||
|
|
||||||
|
textFieldMapping := bleve.NewTextFieldMapping()
|
||||||
|
textFieldMapping.Analyzer = simple.Name
|
||||||
|
docMapping.AddFieldMappingsAt("Title", textFieldMapping)
|
||||||
|
docMapping.AddFieldMappingsAt("Content", textFieldMapping)
|
||||||
|
|
||||||
|
mapping.AddDocumentMapping("issues", docMapping)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
issueIndexer, err = bleve.New(setting.Indexer.IssuePath, mapping)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// populateIssueIndexer populate the issue indexer with issue data
|
||||||
|
func populateIssueIndexer() error {
|
||||||
|
for page := 1; ; page++ {
|
||||||
|
repos, err := Repositories(&SearchRepoOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Repositories: %v", err)
|
||||||
|
}
|
||||||
|
if len(repos) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
batch := issueIndexer.NewBatch()
|
||||||
|
for _, repo := range repos {
|
||||||
|
issues, err := Issues(&IssuesOptions{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
IsClosed: util.OptionalBoolNone,
|
||||||
|
IsPull: util.OptionalBoolNone,
|
||||||
|
Page: -1, // do not page
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Issues: %v", err)
|
||||||
|
}
|
||||||
|
for _, issue := range issues {
|
||||||
|
err = batch.Index(issue.indexUID(), issue.issueData())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("batch.Index: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = issueIndexer.Batch(batch); err != nil {
|
||||||
|
return fmt.Errorf("index.Batch: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processIssueIndexerUpdateQueue() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case issue := <-issueIndexerUpdateQueue:
|
||||||
|
if err := issueIndexer.Index(issue.indexUID(), issue.issueData()); err != nil {
|
||||||
|
log.Error(4, "issuesIndexer.Index: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexUID a unique identifier for an issue used in full-text indices
|
||||||
|
func (issue *Issue) indexUID() string {
|
||||||
|
return strconv.FormatInt(issue.ID, 36)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (issue *Issue) issueData() *issueIndexerData {
|
||||||
|
return &issueIndexerData{
|
||||||
|
ID: issue.ID,
|
||||||
|
RepoID: issue.RepoID,
|
||||||
|
Title: issue.Title,
|
||||||
|
Content: issue.Content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateIssueIndexer add/update an issue to the issue indexer
|
||||||
|
func UpdateIssueIndexer(issue *Issue) {
|
||||||
|
go func() {
|
||||||
|
issueIndexerUpdateQueue <- issue
|
||||||
|
}()
|
||||||
|
}
|
|
@ -138,6 +138,10 @@ func LoadConfigs() {
|
||||||
}
|
}
|
||||||
DbCfg.SSLMode = sec.Key("SSL_MODE").String()
|
DbCfg.SSLMode = sec.Key("SSL_MODE").String()
|
||||||
DbCfg.Path = sec.Key("PATH").MustString("data/gitea.db")
|
DbCfg.Path = sec.Key("PATH").MustString("data/gitea.db")
|
||||||
|
|
||||||
|
sec = setting.Cfg.Section("indexer")
|
||||||
|
setting.Indexer.IssuePath = sec.Key("ISSUE_INDEXER_PATH").MustString("indexers/issues.bleve")
|
||||||
|
setting.Indexer.UpdateQueueLength = sec.Key("UPDATE_BUFFER_LEN").MustInt(20)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsePostgreSQLHostPort parses given input in various forms defined in
|
// parsePostgreSQLHostPort parses given input in various forms defined in
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright 2016 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewContext start indexer service
|
||||||
|
func NewContext() {
|
||||||
|
models.InitIssueIndexer()
|
||||||
|
}
|
|
@ -123,6 +123,12 @@ var (
|
||||||
UsePostgreSQL bool
|
UsePostgreSQL bool
|
||||||
UseTiDB bool
|
UseTiDB bool
|
||||||
|
|
||||||
|
// Indexer settings
|
||||||
|
Indexer struct {
|
||||||
|
IssuePath string
|
||||||
|
UpdateQueueLength int
|
||||||
|
}
|
||||||
|
|
||||||
// Webhook settings
|
// Webhook settings
|
||||||
Webhook = struct {
|
Webhook = struct {
|
||||||
QueueLength int
|
QueueLength int
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
// OptionalBool a boolean that can be "null"
|
||||||
|
type OptionalBool byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OptionalBoolNone a "null" boolean value
|
||||||
|
OptionalBoolNone = iota
|
||||||
|
// OptionalBoolTrue a "true" boolean value
|
||||||
|
OptionalBoolTrue
|
||||||
|
// OptionalBoolFalse a "false" boolean value
|
||||||
|
OptionalBoolFalse
|
||||||
|
)
|
||||||
|
|
||||||
|
// OptionalBoolOf get the corresponding OptionalBool of a bool
|
||||||
|
func OptionalBoolOf(b bool) OptionalBool {
|
||||||
|
if b {
|
||||||
|
return OptionalBoolTrue
|
||||||
|
}
|
||||||
|
return OptionalBoolFalse
|
||||||
|
}
|
|
@ -2926,6 +2926,10 @@ footer .ui.language .menu {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
.ui.repository.list .item {
|
.ui.repository.list .item {
|
||||||
padding-bottom: 25px;
|
padding-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,14 +13,16 @@ import (
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListIssues list the issues of a repository
|
// ListIssues list the issues of a repository
|
||||||
func ListIssues(ctx *context.APIContext) {
|
func ListIssues(ctx *context.APIContext) {
|
||||||
|
isClosed := ctx.Query("state") == "closed"
|
||||||
issueOpts := models.IssuesOptions{
|
issueOpts := models.IssuesOptions{
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
Page: ctx.QueryInt("page"),
|
Page: ctx.QueryInt("page"),
|
||||||
IsClosed: ctx.Query("state") == "closed",
|
IsClosed: util.OptionalBoolOf(isClosed),
|
||||||
}
|
}
|
||||||
|
|
||||||
issues, err := models.Issues(&issueOpts)
|
issues, err := models.Issues(&issueOpts)
|
||||||
|
@ -29,7 +31,7 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ctx.Query("state") == "all" {
|
if ctx.Query("state") == "all" {
|
||||||
issueOpts.IsClosed = !issueOpts.IsClosed
|
issueOpts.IsClosed = util.OptionalBoolOf(!isClosed)
|
||||||
tempIssues, err := models.Issues(&issueOpts)
|
tempIssues, err := models.Issues(&issueOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(500, "Issues", err)
|
ctx.Error(500, "Issues", err)
|
||||||
|
|
|
@ -125,7 +125,7 @@ func DeleteIssueComment(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = models.DeleteCommentByID(comment.ID); err != nil {
|
if err = models.DeleteComment(comment); err != nil {
|
||||||
ctx.Error(500, "DeleteCommentByID", err)
|
ctx.Error(500, "DeleteCommentByID", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/ssh"
|
"code.gitea.io/gitea/modules/ssh"
|
||||||
macaron "gopkg.in/macaron.v1"
|
macaron "gopkg.in/macaron.v1"
|
||||||
|
"code.gitea.io/gitea/modules/indexer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkRunMode() {
|
func checkRunMode() {
|
||||||
|
@ -36,6 +37,7 @@ func checkRunMode() {
|
||||||
func NewServices() {
|
func NewServices() {
|
||||||
setting.NewServices()
|
setting.NewServices()
|
||||||
mailer.NewContext()
|
mailer.NewContext()
|
||||||
|
indexer.NewContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GlobalInit is for global configuration reload-able.
|
// GlobalInit is for global configuration reload-able.
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -25,6 +26,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/markdown"
|
"code.gitea.io/gitea/modules/markdown"
|
||||||
"code.gitea.io/gitea/modules/notification"
|
"code.gitea.io/gitea/modules/notification"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -158,20 +160,39 @@ func Issues(ctx *context.Context) {
|
||||||
milestoneID := ctx.QueryInt64("milestone")
|
milestoneID := ctx.QueryInt64("milestone")
|
||||||
isShowClosed := ctx.Query("state") == "closed"
|
isShowClosed := ctx.Query("state") == "closed"
|
||||||
|
|
||||||
|
keyword := ctx.Query("q")
|
||||||
|
if bytes.Contains([]byte(keyword), []byte{0x00}) {
|
||||||
|
keyword = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var issueIDs []int64
|
||||||
|
var err error
|
||||||
|
if len(keyword) > 0 {
|
||||||
|
issueIDs, err = models.SearchIssuesByKeyword(repo.ID, keyword)
|
||||||
|
if len(issueIDs) == 0 {
|
||||||
|
forceEmpty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var issueStats *models.IssueStats
|
var issueStats *models.IssueStats
|
||||||
if forceEmpty {
|
if forceEmpty {
|
||||||
issueStats = &models.IssueStats{}
|
issueStats = &models.IssueStats{}
|
||||||
} else {
|
} else {
|
||||||
issueStats = models.GetIssueStats(&models.IssueStatsOptions{
|
var err error
|
||||||
|
issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
Labels: selectLabels,
|
Labels: selectLabels,
|
||||||
MilestoneID: milestoneID,
|
MilestoneID: milestoneID,
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: assigneeID,
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
IsPull: isPullList,
|
IsPull: isPullList,
|
||||||
|
IssueIDs: issueIDs,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(500, "GetSearchIssueStats")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
page := ctx.QueryInt("page")
|
page := ctx.QueryInt("page")
|
||||||
if page <= 1 {
|
if page <= 1 {
|
||||||
page = 1
|
page = 1
|
||||||
|
@ -190,7 +211,6 @@ func Issues(ctx *context.Context) {
|
||||||
if forceEmpty {
|
if forceEmpty {
|
||||||
issues = []*models.Issue{}
|
issues = []*models.Issue{}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
|
||||||
issues, err = models.Issues(&models.IssuesOptions{
|
issues, err = models.Issues(&models.IssuesOptions{
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: assigneeID,
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
@ -198,10 +218,11 @@ func Issues(ctx *context.Context) {
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
MilestoneID: milestoneID,
|
MilestoneID: milestoneID,
|
||||||
Page: pager.Current(),
|
Page: pager.Current(),
|
||||||
IsClosed: isShowClosed,
|
IsClosed: util.OptionalBoolOf(isShowClosed),
|
||||||
IsPull: isPullList,
|
IsPull: util.OptionalBoolOf(isPullList),
|
||||||
Labels: selectLabels,
|
Labels: selectLabels,
|
||||||
SortType: sortType,
|
SortType: sortType,
|
||||||
|
IssueIDs: issueIDs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Handle(500, "Issues", err)
|
ctx.Handle(500, "Issues", err)
|
||||||
|
@ -258,6 +279,7 @@ func Issues(ctx *context.Context) {
|
||||||
ctx.Data["MilestoneID"] = milestoneID
|
ctx.Data["MilestoneID"] = milestoneID
|
||||||
ctx.Data["AssigneeID"] = assigneeID
|
ctx.Data["AssigneeID"] = assigneeID
|
||||||
ctx.Data["IsShowClosed"] = isShowClosed
|
ctx.Data["IsShowClosed"] = isShowClosed
|
||||||
|
ctx.Data["Keyword"] = keyword
|
||||||
if isShowClosed {
|
if isShowClosed {
|
||||||
ctx.Data["State"] = "closed"
|
ctx.Data["State"] = "closed"
|
||||||
} else {
|
} else {
|
||||||
|
@ -934,7 +956,7 @@ func DeleteComment(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = models.DeleteCommentByID(comment.ID); err != nil {
|
if err = models.DeleteComment(comment); err != nil {
|
||||||
ctx.Handle(500, "DeleteCommentByID", err)
|
ctx.Handle(500, "DeleteCommentByID", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -277,8 +278,8 @@ func Issues(ctx *context.Context) {
|
||||||
PosterID: posterID,
|
PosterID: posterID,
|
||||||
RepoIDs: repoIDs,
|
RepoIDs: repoIDs,
|
||||||
Page: page,
|
Page: page,
|
||||||
IsClosed: isShowClosed,
|
IsClosed: util.OptionalBoolOf(isShowClosed),
|
||||||
IsPull: isPullList,
|
IsPull: util.OptionalBoolOf(isPullList),
|
||||||
SortType: sortType,
|
SortType: sortType,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
{{template "repo/issue/navbar" .}}
|
{{template "repo/issue/navbar" .}}
|
||||||
|
{{template "repo/issue/search" .}}
|
||||||
<div class="ui right">
|
<div class="ui right">
|
||||||
{{if .PageIsIssueList}}
|
{{if .PageIsIssueList}}
|
||||||
<a class="ui green button" href="{{.RepoLink}}/issues/new">{{.i18n.Tr "repo.issues.new"}}</a>
|
<a class="ui green button" href="{{.RepoLink}}/issues/new">{{.i18n.Tr "repo.issues.new"}}</a>
|
||||||
|
@ -14,11 +15,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="ui tiny basic status buttons">
|
<div class="ui tiny basic status buttons">
|
||||||
<a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
|
<a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
|
||||||
<i class="octicon octicon-issue-opened"></i>
|
<i class="octicon octicon-issue-opened"></i>
|
||||||
{{.i18n.Tr "repo.issues.open_tab" .IssueStats.OpenCount}}
|
{{.i18n.Tr "repo.issues.open_tab" .IssueStats.OpenCount}}
|
||||||
</a>
|
</a>
|
||||||
<a class="ui {{if .IsShowClosed}}red active{{end}} basic button" href="{{$.Link}}?type={{.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
|
<a class="ui {{if .IsShowClosed}}red active{{end}} basic button" href="{{$.Link}}?q={{$.Keyword}}&type={{.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
|
||||||
<i class="octicon octicon-issue-closed"></i>
|
<i class="octicon octicon-issue-closed"></i>
|
||||||
{{.i18n.Tr "repo.issues.close_tab" .IssueStats.ClosedCount}}
|
{{.i18n.Tr "repo.issues.close_tab" .IssueStats.ClosedCount}}
|
||||||
</a>
|
</a>
|
||||||
|
@ -31,9 +32,9 @@
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
</span>
|
</span>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_label_no_select"}}</a>
|
<a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_label_no_select"}}</a>
|
||||||
{{range .Labels}}
|
{{range .Labels}}
|
||||||
<a class="item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.ID}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}"><span class="octicon {{if eq $.SelectLabels .ID}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}</a>
|
<a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.ID}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}"><span class="octicon {{if eq $.SelectLabels .ID}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +46,7 @@
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
</span>
|
</span>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_milestone_no_select"}}</a>
|
<a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_milestone_no_select"}}</a>
|
||||||
{{range .Milestones}}
|
{{range .Milestones}}
|
||||||
<a class="{{if eq $.MilestoneID .ID}}active selected{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&assignee={{$.AssigneeID}}">{{.Name}}</a>
|
<a class="{{if eq $.MilestoneID .ID}}active selected{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&assignee={{$.AssigneeID}}">{{.Name}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -59,7 +60,7 @@
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
</span>
|
</span>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}">{{.i18n.Tr "repo.issues.filter_assginee_no_select"}}</a>
|
<a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}">{{.i18n.Tr "repo.issues.filter_assginee_no_select"}}</a>
|
||||||
{{range .Assignees}}
|
{{range .Assignees}}
|
||||||
<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{.ID}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</a>
|
<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{.ID}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -73,10 +74,10 @@
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
</span>
|
</span>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="{{if eq .ViewType "all"}}active{{end}} item" href="{{$.Link}}?type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.all_issues"}}</a>
|
<a class="{{if eq .ViewType "all"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.all_issues"}}</a>
|
||||||
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{$.Link}}?type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
|
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
|
||||||
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{$.Link}}?type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.created_by_you"}}</a>
|
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.created_by_you"}}</a>
|
||||||
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{$.Link}}?type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.mentioning_you"}}</a>
|
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.mentioning_you"}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -87,12 +88,12 @@
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
</span>
|
</span>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="{{if or (eq .SortType "latest") (not .SortType)}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a>
|
<a class="{{if or (eq .SortType "latest") (not .SortType)}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a>
|
||||||
<a class="{{if eq .SortType "oldest"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a>
|
<a class="{{if eq .SortType "oldest"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a>
|
||||||
<a class="{{if eq .SortType "recentupdate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
<a class="{{if eq .SortType "recentupdate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
||||||
<a class="{{if eq .SortType "leastupdate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
<a class="{{if eq .SortType "leastupdate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
||||||
<a class="{{if eq .SortType "mostcomment"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.mostcomment"}}</a>
|
<a class="{{if eq .SortType "mostcomment"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.mostcomment"}}</a>
|
||||||
<a class="{{if eq .SortType "leastcomment"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastcomment"}}</a>
|
<a class="{{if eq .SortType "leastcomment"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastcomment"}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,7 +106,7 @@
|
||||||
<a class="title has-emoji" href="{{$.Link}}/{{.Index}}">{{.Title}}</a>
|
<a class="title has-emoji" href="{{$.Link}}/{{.Index}}">{{.Title}}</a>
|
||||||
|
|
||||||
{{range .Labels}}
|
{{range .Labels}}
|
||||||
<a class="ui label" href="{{$.Link}}?type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}">{{.Name}}</a>
|
<a class="ui label" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}">{{.Name}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .NumComments}}
|
{{if .NumComments}}
|
||||||
|
@ -115,7 +116,7 @@
|
||||||
<p class="desc">
|
<p class="desc">
|
||||||
{{$.i18n.Tr "repo.issues.opened_by" $timeStr .Poster.HomeLink .Poster.Name | Safe}}
|
{{$.i18n.Tr "repo.issues.opened_by" $timeStr .Poster.HomeLink .Poster.Name | Safe}}
|
||||||
{{if .Milestone}}
|
{{if .Milestone}}
|
||||||
<a class="milestone" href="{{$.Link}}?type={{$.ViewType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.Milestone.ID}}&assignee={{$.AssigneeID}}">
|
<a class="milestone" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.Milestone.ID}}&assignee={{$.AssigneeID}}">
|
||||||
<span class="octicon octicon-milestone"></span> {{.Milestone.Name}}
|
<span class="octicon octicon-milestone"></span> {{.Milestone.Name}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -132,17 +133,17 @@
|
||||||
{{if gt .TotalPages 1}}
|
{{if gt .TotalPages 1}}
|
||||||
<div class="center page buttons">
|
<div class="center page buttons">
|
||||||
<div class="ui borderless pagination menu">
|
<div class="ui borderless pagination menu">
|
||||||
<a class="{{if not .HasPrevious}}disabled{{end}} item" {{if .HasPrevious}}href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}&page={{.Previous}}"{{end}}>
|
<a class="{{if not .HasPrevious}}disabled{{end}} item" {{if .HasPrevious}}href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}&page={{.Previous}}"{{end}}>
|
||||||
<i class="left arrow icon"></i> {{$.i18n.Tr "repo.issues.previous"}}
|
<i class="left arrow icon"></i> {{$.i18n.Tr "repo.issues.previous"}}
|
||||||
</a>
|
</a>
|
||||||
{{range .Pages}}
|
{{range .Pages}}
|
||||||
{{if eq .Num -1}}
|
{{if eq .Num -1}}
|
||||||
<a class="disabled item">...</a>
|
<a class="disabled item">...</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a class="{{if .IsCurrent}}active{{end}} item" {{if not .IsCurrent}}href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}&page={{.Num}}"{{end}}>{{.Num}}</a>
|
<a class="{{if .IsCurrent}}active{{end}} item" {{if not .IsCurrent}}href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}&page={{.Num}}"{{end}}>{{.Num}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<a class="{{if not .HasNext}}disabled{{end}} item" {{if .HasNext}}href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}&page={{.Next}}"{{end}}>
|
<a class="{{if not .HasNext}}disabled{{end}} item" {{if .HasNext}}href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}&page={{.Next}}"{{end}}>
|
||||||
{{$.i18n.Tr "repo.issues.next"}} <i class="icon right arrow"></i>
|
{{$.i18n.Tr "repo.issues.next"}} <i class="icon right arrow"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="ui compact small menu">
|
<div class="ui compact left small menu">
|
||||||
<a class="{{if .PageIsLabels}}active{{end}} item" href="{{.RepoLink}}/labels">{{.i18n.Tr "repo.labels"}}</a>
|
<a class="{{if .PageIsLabels}}active{{end}} item" href="{{.RepoLink}}/labels">{{.i18n.Tr "repo.labels"}}</a>
|
||||||
<a class="{{if .PageIsMilestones}}active{{end}} item" href="{{.RepoLink}}/milestones">{{.i18n.Tr "repo.milestones"}}</a>
|
<a class="{{if .PageIsMilestones}}active{{end}} item" href="{{.RepoLink}}/milestones">{{.i18n.Tr "repo.milestones"}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<form class="ui form">
|
||||||
|
<div class="ui fluid action input">
|
||||||
|
<input type="hidden" name="type" value="{{$.ViewType}}"/>
|
||||||
|
<input type="hidden" name="state" value="{{$.State}}"/>
|
||||||
|
<input type="hidden" name="labels" value="{{.SelectLabels}}"/>
|
||||||
|
<input type="hidden" name="milestone" value="{{$.MilestoneID}}"/>
|
||||||
|
<input type="hidden" name="assignee" value="{{$.AssigneeID}}"/>
|
||||||
|
<div class="ui search action input">
|
||||||
|
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus>
|
||||||
|
</div>
|
||||||
|
<button class="ui blue button" type="submit">{{.i18n.Tr "explore.search"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Contributing to Bleve
|
||||||
|
|
||||||
|
We look forward to your contributions, but ask that you first review these guidelines.
|
||||||
|
|
||||||
|
### Sign the CLA
|
||||||
|
|
||||||
|
As Bleve is a Couchbase project we require contributors accept the [Couchbase Contributor License Agreement](http://review.couchbase.org/static/individual_agreement.html). To sign this agreement log into the Couchbase [code review tool](http://review.couchbase.org/). The Bleve project does not use this code review tool but it is still used to track acceptance of the contributor license agreements.
|
||||||
|
|
||||||
|
### Submitting a Pull Request
|
||||||
|
|
||||||
|
All types of contributions are welcome, but please keep the following in mind:
|
||||||
|
|
||||||
|
- If you're planning a large change, you should really discuss it in a github issue or on the google group first. This helps avoid duplicate effort and spending time on something that may not be merged.
|
||||||
|
- Existing tests should continue to pass, new tests for the contribution are nice to have.
|
||||||
|
- All code should have gone through `go fmt`
|
||||||
|
- All code should pass `go vet`
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,62 @@
|
||||||
|
# ![bleve](docs/bleve.png) bleve
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/blevesearch/bleve.svg?branch=master)](https://travis-ci.org/blevesearch/bleve) [![Coverage Status](https://coveralls.io/repos/blevesearch/bleve/badge.png?branch=master)](https://coveralls.io/r/blevesearch/bleve?branch=master) [![GoDoc](https://godoc.org/github.com/blevesearch/bleve?status.svg)](https://godoc.org/github.com/blevesearch/bleve)
|
||||||
|
[![Join the chat at https://gitter.im/blevesearch/bleve](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/blevesearch/bleve?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
[![codebeat](https://codebeat.co/badges/38a7cbc9-9cf5-41c0-a315-0746178230f4)](https://codebeat.co/projects/github-com-blevesearch-bleve)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/blevesearch/bleve)](https://goreportcard.com/report/blevesearch/bleve)
|
||||||
|
|
||||||
|
modern text indexing in go - [blevesearch.com](http://www.blevesearch.com/)
|
||||||
|
|
||||||
|
Try out bleve live by [searching the bleve website](http://www.blevesearch.com/search/?q=bleve).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Index any go data structure (including JSON)
|
||||||
|
* Intelligent defaults backed up by powerful configuration
|
||||||
|
* Supported field types:
|
||||||
|
* Text, Numeric, Date
|
||||||
|
* Supported query types:
|
||||||
|
* Term, Phrase, Match, Match Phrase, Prefix
|
||||||
|
* Conjunction, Disjunction, Boolean
|
||||||
|
* Numeric Range, Date Range
|
||||||
|
* Simple query [syntax](http://www.blevesearch.com/docs/Query-String-Query/) for human entry
|
||||||
|
* tf-idf Scoring
|
||||||
|
* Search result match highlighting
|
||||||
|
* Supports Aggregating Facets:
|
||||||
|
* Terms Facet
|
||||||
|
* Numeric Range Facet
|
||||||
|
* Date Range Facet
|
||||||
|
|
||||||
|
## Discussion
|
||||||
|
|
||||||
|
Discuss usage and development of bleve in the [google group](https://groups.google.com/forum/#!forum/bleve).
|
||||||
|
|
||||||
|
## Indexing
|
||||||
|
|
||||||
|
message := struct{
|
||||||
|
Id string
|
||||||
|
From string
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Id: "example",
|
||||||
|
From: "marty.schoch@gmail.com",
|
||||||
|
Body: "bleve indexing is easy",
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping := bleve.NewIndexMapping()
|
||||||
|
index, err := bleve.New("example.bleve", mapping)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
index.Index(message.Id, message)
|
||||||
|
|
||||||
|
## Querying
|
||||||
|
|
||||||
|
index, _ := bleve.Open("example.bleve")
|
||||||
|
query := bleve.NewQueryStringQuery("bleve")
|
||||||
|
searchRequest := bleve.NewSearchRequest(query)
|
||||||
|
searchResult, _ := index.Search(searchRequest)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache License Version 2.0
|
46
vendor/github.com/blevesearch/bleve/analysis/analyzer/simple/simple.go
generated
vendored
Normal file
46
vendor/github.com/blevesearch/bleve/analysis/analyzer/simple/simple.go
generated
vendored
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package simple
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/analysis/token/lowercase"
|
||||||
|
"github.com/blevesearch/bleve/analysis/tokenizer/letter"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "simple"
|
||||||
|
|
||||||
|
func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
|
||||||
|
tokenizer, err := cache.TokenizerNamed(letter.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv := analysis.Analyzer{
|
||||||
|
Tokenizer: tokenizer,
|
||||||
|
TokenFilters: []analysis.TokenFilter{
|
||||||
|
toLowerFilter,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterAnalyzer(Name, AnalyzerConstructor)
|
||||||
|
}
|
52
vendor/github.com/blevesearch/bleve/analysis/analyzer/standard/standard.go
generated
vendored
Normal file
52
vendor/github.com/blevesearch/bleve/analysis/analyzer/standard/standard.go
generated
vendored
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/analysis/lang/en"
|
||||||
|
"github.com/blevesearch/bleve/analysis/token/lowercase"
|
||||||
|
"github.com/blevesearch/bleve/analysis/tokenizer/unicode"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "standard"
|
||||||
|
|
||||||
|
func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
|
||||||
|
tokenizer, err := cache.TokenizerNamed(unicode.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stopEnFilter, err := cache.TokenFilterNamed(en.StopName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv := analysis.Analyzer{
|
||||||
|
Tokenizer: tokenizer,
|
||||||
|
TokenFilters: []analysis.TokenFilter{
|
||||||
|
toLowerFilter,
|
||||||
|
stopEnFilter,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterAnalyzer(Name, AnalyzerConstructor)
|
||||||
|
}
|
64
vendor/github.com/blevesearch/bleve/analysis/datetime/flexible/flexible.go
generated
vendored
Normal file
64
vendor/github.com/blevesearch/bleve/analysis/datetime/flexible/flexible.go
generated
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package flexible
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "flexiblego"
|
||||||
|
|
||||||
|
type DateTimeParser struct {
|
||||||
|
layouts []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(layouts []string) *DateTimeParser {
|
||||||
|
return &DateTimeParser{
|
||||||
|
layouts: layouts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DateTimeParser) ParseDateTime(input string) (time.Time, error) {
|
||||||
|
for _, layout := range p.layouts {
|
||||||
|
rv, err := time.Parse(layout, input)
|
||||||
|
if err == nil {
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}, analysis.ErrInvalidDateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
|
||||||
|
layouts, ok := config["layouts"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("must specify layouts")
|
||||||
|
}
|
||||||
|
var layoutStrs []string
|
||||||
|
for _, layout := range layouts {
|
||||||
|
layoutStr, ok := layout.(string)
|
||||||
|
if ok {
|
||||||
|
layoutStrs = append(layoutStrs, layoutStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return New(layoutStrs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
|
||||||
|
}
|
45
vendor/github.com/blevesearch/bleve/analysis/datetime/optional/optional.go
generated
vendored
Normal file
45
vendor/github.com/blevesearch/bleve/analysis/datetime/optional/optional.go
generated
vendored
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package optional
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/analysis/datetime/flexible"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "dateTimeOptional"
|
||||||
|
|
||||||
|
const rfc3339NoTimezone = "2006-01-02T15:04:05"
|
||||||
|
const rfc3339NoTimezoneNoT = "2006-01-02 15:04:05"
|
||||||
|
const rfc3339NoTime = "2006-01-02"
|
||||||
|
|
||||||
|
var layouts = []string{
|
||||||
|
time.RFC3339Nano,
|
||||||
|
time.RFC3339,
|
||||||
|
rfc3339NoTimezone,
|
||||||
|
rfc3339NoTimezoneNoT,
|
||||||
|
rfc3339NoTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
|
||||||
|
return flexible.New(layouts), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package analysis
|
||||||
|
|
||||||
|
// TokenLocation represents one occurrence of a term at a particular location in
|
||||||
|
// a field. Start, End and Position have the same meaning as in analysis.Token.
|
||||||
|
// Field and ArrayPositions identify the field value in the source document.
|
||||||
|
// See document.Field for details.
|
||||||
|
type TokenLocation struct {
|
||||||
|
Field string
|
||||||
|
ArrayPositions []uint64
|
||||||
|
Start int
|
||||||
|
End int
|
||||||
|
Position int
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenFreq represents all the occurrences of a term in all fields of a
|
||||||
|
// document.
|
||||||
|
type TokenFreq struct {
|
||||||
|
Term []byte
|
||||||
|
Locations []*TokenLocation
|
||||||
|
frequency int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tf *TokenFreq) Frequency() int {
|
||||||
|
return tf.frequency
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenFrequencies maps document terms to their combined frequencies from all
|
||||||
|
// fields.
|
||||||
|
type TokenFrequencies map[string]*TokenFreq
|
||||||
|
|
||||||
|
func (tfs TokenFrequencies) MergeAll(remoteField string, other TokenFrequencies) {
|
||||||
|
// walk the new token frequencies
|
||||||
|
for tfk, tf := range other {
|
||||||
|
// set the remoteField value in incoming token freqs
|
||||||
|
for _, l := range tf.Locations {
|
||||||
|
l.Field = remoteField
|
||||||
|
}
|
||||||
|
existingTf, exists := tfs[tfk]
|
||||||
|
if exists {
|
||||||
|
existingTf.Locations = append(existingTf.Locations, tf.Locations...)
|
||||||
|
existingTf.frequency = existingTf.frequency + tf.frequency
|
||||||
|
} else {
|
||||||
|
tfs[tfk] = &TokenFreq{
|
||||||
|
Term: tf.Term,
|
||||||
|
frequency: tf.frequency,
|
||||||
|
Locations: make([]*TokenLocation, len(tf.Locations)),
|
||||||
|
}
|
||||||
|
copy(tfs[tfk].Locations, tf.Locations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TokenFrequency(tokens TokenStream, arrayPositions []uint64, includeTermVectors bool) TokenFrequencies {
|
||||||
|
rv := make(map[string]*TokenFreq, len(tokens))
|
||||||
|
|
||||||
|
if includeTermVectors {
|
||||||
|
tls := make([]TokenLocation, len(tokens))
|
||||||
|
tlNext := 0
|
||||||
|
|
||||||
|
for _, token := range tokens {
|
||||||
|
tls[tlNext] = TokenLocation{
|
||||||
|
ArrayPositions: arrayPositions,
|
||||||
|
Start: token.Start,
|
||||||
|
End: token.End,
|
||||||
|
Position: token.Position,
|
||||||
|
}
|
||||||
|
|
||||||
|
curr, ok := rv[string(token.Term)]
|
||||||
|
if ok {
|
||||||
|
curr.Locations = append(curr.Locations, &tls[tlNext])
|
||||||
|
curr.frequency++
|
||||||
|
} else {
|
||||||
|
rv[string(token.Term)] = &TokenFreq{
|
||||||
|
Term: token.Term,
|
||||||
|
Locations: []*TokenLocation{&tls[tlNext]},
|
||||||
|
frequency: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tlNext++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, token := range tokens {
|
||||||
|
curr, exists := rv[string(token.Term)]
|
||||||
|
if exists {
|
||||||
|
curr.frequency++
|
||||||
|
} else {
|
||||||
|
rv[string(token.Term)] = &TokenFreq{
|
||||||
|
Term: token.Term,
|
||||||
|
frequency: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv
|
||||||
|
}
|
70
vendor/github.com/blevesearch/bleve/analysis/lang/en/analyzer_en.go
generated
vendored
Normal file
70
vendor/github.com/blevesearch/bleve/analysis/lang/en/analyzer_en.go
generated
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package en implements an analyzer with reasonable defaults for processing
|
||||||
|
// English text.
|
||||||
|
//
|
||||||
|
// It strips possessive suffixes ('s), transforms tokens to lower case,
|
||||||
|
// removes stopwords from a built-in list, and applies porter stemming.
|
||||||
|
//
|
||||||
|
// The built-in stopwords list is defined in EnglishStopWords.
|
||||||
|
package en
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis/token/lowercase"
|
||||||
|
"github.com/blevesearch/bleve/analysis/token/porter"
|
||||||
|
"github.com/blevesearch/bleve/analysis/tokenizer/unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AnalyzerName = "en"
|
||||||
|
|
||||||
|
func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
|
||||||
|
tokenizer, err := cache.TokenizerNamed(unicode.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
possEnFilter, err := cache.TokenFilterNamed(PossessiveName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stopEnFilter, err := cache.TokenFilterNamed(StopName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stemmerEnFilter, err := cache.TokenFilterNamed(porter.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv := analysis.Analyzer{
|
||||||
|
Tokenizer: tokenizer,
|
||||||
|
TokenFilters: []analysis.TokenFilter{
|
||||||
|
possEnFilter,
|
||||||
|
toLowerFilter,
|
||||||
|
stopEnFilter,
|
||||||
|
stemmerEnFilter,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor)
|
||||||
|
}
|
67
vendor/github.com/blevesearch/bleve/analysis/lang/en/possessive_filter_en.go
generated
vendored
Normal file
67
vendor/github.com/blevesearch/bleve/analysis/lang/en/possessive_filter_en.go
generated
vendored
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package en
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PossessiveName is the name PossessiveFilter is registered as
|
||||||
|
// in the bleve registry.
|
||||||
|
const PossessiveName = "possessive_en"
|
||||||
|
|
||||||
|
const rightSingleQuotationMark = '’'
|
||||||
|
const apostrophe = '\''
|
||||||
|
const fullWidthApostrophe = '''
|
||||||
|
|
||||||
|
const apostropheChars = rightSingleQuotationMark + apostrophe + fullWidthApostrophe
|
||||||
|
|
||||||
|
// PossessiveFilter implements a TokenFilter which
|
||||||
|
// strips the English possessive suffix ('s) from tokens.
|
||||||
|
// It handle a variety of apostrophe types, is case-insensitive
|
||||||
|
// and doesn't distinguish between possessive and contraction.
|
||||||
|
// (ie "She's So Rad" becomes "She So Rad")
|
||||||
|
type PossessiveFilter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPossessiveFilter() *PossessiveFilter {
|
||||||
|
return &PossessiveFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PossessiveFilter) Filter(input analysis.TokenStream) analysis.TokenStream {
|
||||||
|
for _, token := range input {
|
||||||
|
lastRune, lastRuneSize := utf8.DecodeLastRune(token.Term)
|
||||||
|
if lastRune == 's' || lastRune == 'S' {
|
||||||
|
nextLastRune, nextLastRuneSize := utf8.DecodeLastRune(token.Term[:len(token.Term)-lastRuneSize])
|
||||||
|
if nextLastRune == rightSingleQuotationMark ||
|
||||||
|
nextLastRune == apostrophe ||
|
||||||
|
nextLastRune == fullWidthApostrophe {
|
||||||
|
token.Term = token.Term[:len(token.Term)-lastRuneSize-nextLastRuneSize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
func PossessiveFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) {
|
||||||
|
return NewPossessiveFilter(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterTokenFilter(PossessiveName, PossessiveFilterConstructor)
|
||||||
|
}
|
33
vendor/github.com/blevesearch/bleve/analysis/lang/en/stop_filter_en.go
generated
vendored
Normal file
33
vendor/github.com/blevesearch/bleve/analysis/lang/en/stop_filter_en.go
generated
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package en
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/analysis/token/stop"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) {
|
||||||
|
tokenMap, err := cache.TokenMapNamed(StopName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return stop.NewStopTokensFilter(tokenMap), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor)
|
||||||
|
}
|
344
vendor/github.com/blevesearch/bleve/analysis/lang/en/stop_words_en.go
generated
vendored
Normal file
344
vendor/github.com/blevesearch/bleve/analysis/lang/en/stop_words_en.go
generated
vendored
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
package en
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const StopName = "stop_en"
|
||||||
|
|
||||||
|
// EnglishStopWords is the built-in list of stopwords used by the "stop_en" TokenFilter.
|
||||||
|
//
|
||||||
|
// this content was obtained from:
|
||||||
|
// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/
|
||||||
|
// ` was changed to ' to allow for literal string
|
||||||
|
var EnglishStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/english/stop.txt
|
||||||
|
| This file is distributed under the BSD License.
|
||||||
|
| See http://snowball.tartarus.org/license.php
|
||||||
|
| Also see http://www.opensource.org/licenses/bsd-license.html
|
||||||
|
| - Encoding was converted to UTF-8.
|
||||||
|
| - This notice was added.
|
||||||
|
|
|
||||||
|
| NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
|
||||||
|
|
||||||
|
| An English stop word list. Comments begin with vertical bar. Each stop
|
||||||
|
| word is at the start of a line.
|
||||||
|
|
||||||
|
| Many of the forms below are quite rare (e.g. "yourselves") but included for
|
||||||
|
| completeness.
|
||||||
|
|
||||||
|
| PRONOUNS FORMS
|
||||||
|
| 1st person sing
|
||||||
|
|
||||||
|
i | subject, always in upper case of course
|
||||||
|
|
||||||
|
me | object
|
||||||
|
my | possessive adjective
|
||||||
|
| the possessive pronoun 'mine' is best suppressed, because of the
|
||||||
|
| sense of coal-mine etc.
|
||||||
|
myself | reflexive
|
||||||
|
| 1st person plural
|
||||||
|
we | subject
|
||||||
|
|
||||||
|
| us | object
|
||||||
|
| care is required here because US = United States. It is usually
|
||||||
|
| safe to remove it if it is in lower case.
|
||||||
|
our | possessive adjective
|
||||||
|
ours | possessive pronoun
|
||||||
|
ourselves | reflexive
|
||||||
|
| second person (archaic 'thou' forms not included)
|
||||||
|
you | subject and object
|
||||||
|
your | possessive adjective
|
||||||
|
yours | possessive pronoun
|
||||||
|
yourself | reflexive (singular)
|
||||||
|
yourselves | reflexive (plural)
|
||||||
|
| third person singular
|
||||||
|
he | subject
|
||||||
|
him | object
|
||||||
|
his | possessive adjective and pronoun
|
||||||
|
himself | reflexive
|
||||||
|
|
||||||
|
she | subject
|
||||||
|
her | object and possessive adjective
|
||||||
|
hers | possessive pronoun
|
||||||
|
herself | reflexive
|
||||||
|
|
||||||
|
it | subject and object
|
||||||
|
its | possessive adjective
|
||||||
|
itself | reflexive
|
||||||
|
| third person plural
|
||||||
|
they | subject
|
||||||
|
them | object
|
||||||
|
their | possessive adjective
|
||||||
|
theirs | possessive pronoun
|
||||||
|
themselves | reflexive
|
||||||
|
| other forms (demonstratives, interrogatives)
|
||||||
|
what
|
||||||
|
which
|
||||||
|
who
|
||||||
|
whom
|
||||||
|
this
|
||||||
|
that
|
||||||
|
these
|
||||||
|
those
|
||||||
|
|
||||||
|
| VERB FORMS (using F.R. Palmer's nomenclature)
|
||||||
|
| BE
|
||||||
|
am | 1st person, present
|
||||||
|
is | -s form (3rd person, present)
|
||||||
|
are | present
|
||||||
|
was | 1st person, past
|
||||||
|
were | past
|
||||||
|
be | infinitive
|
||||||
|
been | past participle
|
||||||
|
being | -ing form
|
||||||
|
| HAVE
|
||||||
|
have | simple
|
||||||
|
has | -s form
|
||||||
|
had | past
|
||||||
|
having | -ing form
|
||||||
|
| DO
|
||||||
|
do | simple
|
||||||
|
does | -s form
|
||||||
|
did | past
|
||||||
|
doing | -ing form
|
||||||
|
|
||||||
|
| The forms below are, I believe, best omitted, because of the significant
|
||||||
|
| homonym forms:
|
||||||
|
|
||||||
|
| He made a WILL
|
||||||
|
| old tin CAN
|
||||||
|
| merry month of MAY
|
||||||
|
| a smell of MUST
|
||||||
|
| fight the good fight with all thy MIGHT
|
||||||
|
|
||||||
|
| would, could, should, ought might however be included
|
||||||
|
|
||||||
|
| | AUXILIARIES
|
||||||
|
| | WILL
|
||||||
|
|will
|
||||||
|
|
||||||
|
would
|
||||||
|
|
||||||
|
| | SHALL
|
||||||
|
|shall
|
||||||
|
|
||||||
|
should
|
||||||
|
|
||||||
|
| | CAN
|
||||||
|
|can
|
||||||
|
|
||||||
|
could
|
||||||
|
|
||||||
|
| | MAY
|
||||||
|
|may
|
||||||
|
|might
|
||||||
|
| | MUST
|
||||||
|
|must
|
||||||
|
| | OUGHT
|
||||||
|
|
||||||
|
ought
|
||||||
|
|
||||||
|
| COMPOUND FORMS, increasingly encountered nowadays in 'formal' writing
|
||||||
|
| pronoun + verb
|
||||||
|
|
||||||
|
i'm
|
||||||
|
you're
|
||||||
|
he's
|
||||||
|
she's
|
||||||
|
it's
|
||||||
|
we're
|
||||||
|
they're
|
||||||
|
i've
|
||||||
|
you've
|
||||||
|
we've
|
||||||
|
they've
|
||||||
|
i'd
|
||||||
|
you'd
|
||||||
|
he'd
|
||||||
|
she'd
|
||||||
|
we'd
|
||||||
|
they'd
|
||||||
|
i'll
|
||||||
|
you'll
|
||||||
|
he'll
|
||||||
|
she'll
|
||||||
|
we'll
|
||||||
|
they'll
|
||||||
|
|
||||||
|
| verb + negation
|
||||||
|
|
||||||
|
isn't
|
||||||
|
aren't
|
||||||
|
wasn't
|
||||||
|
weren't
|
||||||
|
hasn't
|
||||||
|
haven't
|
||||||
|
hadn't
|
||||||
|
doesn't
|
||||||
|
don't
|
||||||
|
didn't
|
||||||
|
|
||||||
|
| auxiliary + negation
|
||||||
|
|
||||||
|
won't
|
||||||
|
wouldn't
|
||||||
|
shan't
|
||||||
|
shouldn't
|
||||||
|
can't
|
||||||
|
cannot
|
||||||
|
couldn't
|
||||||
|
mustn't
|
||||||
|
|
||||||
|
| miscellaneous forms
|
||||||
|
|
||||||
|
let's
|
||||||
|
that's
|
||||||
|
who's
|
||||||
|
what's
|
||||||
|
here's
|
||||||
|
there's
|
||||||
|
when's
|
||||||
|
where's
|
||||||
|
why's
|
||||||
|
how's
|
||||||
|
|
||||||
|
| rarer forms
|
||||||
|
|
||||||
|
| daren't needn't
|
||||||
|
|
||||||
|
| doubtful forms
|
||||||
|
|
||||||
|
| oughtn't mightn't
|
||||||
|
|
||||||
|
| ARTICLES
|
||||||
|
a
|
||||||
|
an
|
||||||
|
the
|
||||||
|
|
||||||
|
| THE REST (Overlap among prepositions, conjunctions, adverbs etc is so
|
||||||
|
| high, that classification is pointless.)
|
||||||
|
and
|
||||||
|
but
|
||||||
|
if
|
||||||
|
or
|
||||||
|
because
|
||||||
|
as
|
||||||
|
until
|
||||||
|
while
|
||||||
|
|
||||||
|
of
|
||||||
|
at
|
||||||
|
by
|
||||||
|
for
|
||||||
|
with
|
||||||
|
about
|
||||||
|
against
|
||||||
|
between
|
||||||
|
into
|
||||||
|
through
|
||||||
|
during
|
||||||
|
before
|
||||||
|
after
|
||||||
|
above
|
||||||
|
below
|
||||||
|
to
|
||||||
|
from
|
||||||
|
up
|
||||||
|
down
|
||||||
|
in
|
||||||
|
out
|
||||||
|
on
|
||||||
|
off
|
||||||
|
over
|
||||||
|
under
|
||||||
|
|
||||||
|
again
|
||||||
|
further
|
||||||
|
then
|
||||||
|
once
|
||||||
|
|
||||||
|
here
|
||||||
|
there
|
||||||
|
when
|
||||||
|
where
|
||||||
|
why
|
||||||
|
how
|
||||||
|
|
||||||
|
all
|
||||||
|
any
|
||||||
|
both
|
||||||
|
each
|
||||||
|
few
|
||||||
|
more
|
||||||
|
most
|
||||||
|
other
|
||||||
|
some
|
||||||
|
such
|
||||||
|
|
||||||
|
no
|
||||||
|
nor
|
||||||
|
not
|
||||||
|
only
|
||||||
|
own
|
||||||
|
same
|
||||||
|
so
|
||||||
|
than
|
||||||
|
too
|
||||||
|
very
|
||||||
|
|
||||||
|
| Just for the record, the following words are among the commonest in English
|
||||||
|
|
||||||
|
| one
|
||||||
|
| every
|
||||||
|
| least
|
||||||
|
| less
|
||||||
|
| many
|
||||||
|
| now
|
||||||
|
| ever
|
||||||
|
| never
|
||||||
|
| say
|
||||||
|
| says
|
||||||
|
| said
|
||||||
|
| also
|
||||||
|
| get
|
||||||
|
| go
|
||||||
|
| goes
|
||||||
|
| just
|
||||||
|
| made
|
||||||
|
| make
|
||||||
|
| put
|
||||||
|
| see
|
||||||
|
| seen
|
||||||
|
| whether
|
||||||
|
| like
|
||||||
|
| well
|
||||||
|
| back
|
||||||
|
| even
|
||||||
|
| still
|
||||||
|
| way
|
||||||
|
| take
|
||||||
|
| since
|
||||||
|
| another
|
||||||
|
| however
|
||||||
|
| two
|
||||||
|
| three
|
||||||
|
| four
|
||||||
|
| five
|
||||||
|
| first
|
||||||
|
| second
|
||||||
|
| new
|
||||||
|
| old
|
||||||
|
| high
|
||||||
|
| long
|
||||||
|
`)
|
||||||
|
|
||||||
|
func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) {
|
||||||
|
rv := analysis.NewTokenMap()
|
||||||
|
err := rv.LoadBytes(EnglishStopWords)
|
||||||
|
return rv, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterTokenMap(StopName, TokenMapConstructor)
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
# full line comment
|
||||||
|
marty
|
||||||
|
steve # trailing comment
|
||||||
|
| different format of comment
|
||||||
|
dustin
|
||||||
|
siri | different style trailing comment
|
||||||
|
multiple words with different whitespace
|
105
vendor/github.com/blevesearch/bleve/analysis/token/lowercase/lowercase.go
generated
vendored
Normal file
105
vendor/github.com/blevesearch/bleve/analysis/token/lowercase/lowercase.go
generated
vendored
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package lowercase implements a TokenFilter which converts
|
||||||
|
// tokens to lower case according to unicode rules.
|
||||||
|
package lowercase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Name is the name used to register LowerCaseFilter in the bleve registry
|
||||||
|
const Name = "to_lower"
|
||||||
|
|
||||||
|
type LowerCaseFilter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLowerCaseFilter() *LowerCaseFilter {
|
||||||
|
return &LowerCaseFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *LowerCaseFilter) Filter(input analysis.TokenStream) analysis.TokenStream {
|
||||||
|
for _, token := range input {
|
||||||
|
token.Term = toLowerDeferredCopy(token.Term)
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
func LowerCaseFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) {
|
||||||
|
return NewLowerCaseFilter(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterTokenFilter(Name, LowerCaseFilterConstructor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// toLowerDeferredCopy will function exactly like
|
||||||
|
// bytes.ToLower() only it will reuse (overwrite)
|
||||||
|
// the original byte array when possible
|
||||||
|
// NOTE: because its possible that the lower-case
|
||||||
|
// form of a rune has a different utf-8 encoded
|
||||||
|
// length, in these cases a new byte array is allocated
|
||||||
|
func toLowerDeferredCopy(s []byte) []byte {
|
||||||
|
j := 0
|
||||||
|
for i := 0; i < len(s); {
|
||||||
|
wid := 1
|
||||||
|
r := rune(s[i])
|
||||||
|
if r >= utf8.RuneSelf {
|
||||||
|
r, wid = utf8.DecodeRune(s[i:])
|
||||||
|
}
|
||||||
|
|
||||||
|
l := unicode.ToLower(r)
|
||||||
|
|
||||||
|
// If the rune is already lowercased, just move to the
|
||||||
|
// next rune.
|
||||||
|
if l == r {
|
||||||
|
i += wid
|
||||||
|
j += wid
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the Unicode edge-case where the last
|
||||||
|
// rune in a word on the greek Σ needs to be converted
|
||||||
|
// differently.
|
||||||
|
if l == 'σ' && i+2 == len(s) {
|
||||||
|
l = 'ς'
|
||||||
|
}
|
||||||
|
|
||||||
|
lwid := utf8.RuneLen(l)
|
||||||
|
if lwid > wid {
|
||||||
|
// utf-8 encoded replacement is wider
|
||||||
|
// for now, punt and defer
|
||||||
|
// to bytes.ToLower() for the remainder
|
||||||
|
// only known to happen with chars
|
||||||
|
// Rune Ⱥ(570) width 2 - Lower ⱥ(11365) width 3
|
||||||
|
// Rune Ⱦ(574) width 2 - Lower ⱦ(11366) width 3
|
||||||
|
rest := bytes.ToLower(s[i:])
|
||||||
|
rv := make([]byte, j+len(rest))
|
||||||
|
copy(rv[:j], s[:j])
|
||||||
|
copy(rv[j:], rest)
|
||||||
|
return rv
|
||||||
|
} else {
|
||||||
|
utf8.EncodeRune(s[j:], l)
|
||||||
|
}
|
||||||
|
i += wid
|
||||||
|
j += lwid
|
||||||
|
}
|
||||||
|
return s[:j]
|
||||||
|
}
|
53
vendor/github.com/blevesearch/bleve/analysis/token/porter/porter.go
generated
vendored
Normal file
53
vendor/github.com/blevesearch/bleve/analysis/token/porter/porter.go
generated
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package porter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
|
||||||
|
"github.com/blevesearch/go-porterstemmer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "stemmer_porter"
|
||||||
|
|
||||||
|
type PorterStemmer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPorterStemmer() *PorterStemmer {
|
||||||
|
return &PorterStemmer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PorterStemmer) Filter(input analysis.TokenStream) analysis.TokenStream {
|
||||||
|
for _, token := range input {
|
||||||
|
// if it is not a protected keyword, stem it
|
||||||
|
if !token.KeyWord {
|
||||||
|
termRunes := bytes.Runes(token.Term)
|
||||||
|
stemmedRunes := porterstemmer.StemWithoutLowerCasing(termRunes)
|
||||||
|
token.Term = analysis.BuildTermFromRunes(stemmedRunes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
func PorterStemmerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) {
|
||||||
|
return NewPorterStemmer(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterTokenFilter(Name, PorterStemmerConstructor)
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package stop implements a TokenFilter removing tokens found in
|
||||||
|
// a TokenMap.
|
||||||
|
//
|
||||||
|
// It constructor takes the following arguments:
|
||||||
|
//
|
||||||
|
// "stop_token_map" (string): the name of the token map identifying tokens to
|
||||||
|
// remove.
|
||||||
|
package stop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "stop_tokens"
|
||||||
|
|
||||||
|
type StopTokensFilter struct {
|
||||||
|
stopTokens analysis.TokenMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStopTokensFilter(stopTokens analysis.TokenMap) *StopTokensFilter {
|
||||||
|
return &StopTokensFilter{
|
||||||
|
stopTokens: stopTokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *StopTokensFilter) Filter(input analysis.TokenStream) analysis.TokenStream {
|
||||||
|
j := 0
|
||||||
|
for _, token := range input {
|
||||||
|
_, isStopToken := f.stopTokens[string(token.Term)]
|
||||||
|
if !isStopToken {
|
||||||
|
input[j] = token
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return input[:j]
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopTokensFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) {
|
||||||
|
stopTokenMapName, ok := config["stop_token_map"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("must specify stop_token_map")
|
||||||
|
}
|
||||||
|
stopTokenMap, err := cache.TokenMapNamed(stopTokenMapName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error building stop words filter: %v", err)
|
||||||
|
}
|
||||||
|
return NewStopTokensFilter(stopTokenMap), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterTokenFilter(Name, StopTokensFilterConstructor)
|
||||||
|
}
|
76
vendor/github.com/blevesearch/bleve/analysis/tokenizer/character/character.go
generated
vendored
Normal file
76
vendor/github.com/blevesearch/bleve/analysis/tokenizer/character/character.go
generated
vendored
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright (c) 2016 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package character
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IsTokenRune func(r rune) bool
|
||||||
|
|
||||||
|
type CharacterTokenizer struct {
|
||||||
|
isTokenRun IsTokenRune
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCharacterTokenizer(f IsTokenRune) *CharacterTokenizer {
|
||||||
|
return &CharacterTokenizer{
|
||||||
|
isTokenRun: f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CharacterTokenizer) Tokenize(input []byte) analysis.TokenStream {
|
||||||
|
|
||||||
|
rv := make(analysis.TokenStream, 0, 1024)
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
start := 0
|
||||||
|
end := 0
|
||||||
|
count := 0
|
||||||
|
for currRune, size := utf8.DecodeRune(input[offset:]); currRune != utf8.RuneError; currRune, size = utf8.DecodeRune(input[offset:]) {
|
||||||
|
isToken := c.isTokenRun(currRune)
|
||||||
|
if isToken {
|
||||||
|
end = offset + size
|
||||||
|
} else {
|
||||||
|
if end-start > 0 {
|
||||||
|
// build token
|
||||||
|
rv = append(rv, &analysis.Token{
|
||||||
|
Term: input[start:end],
|
||||||
|
Start: start,
|
||||||
|
End: end,
|
||||||
|
Position: count + 1,
|
||||||
|
Type: analysis.AlphaNumeric,
|
||||||
|
})
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
start = offset + size
|
||||||
|
end = start
|
||||||
|
}
|
||||||
|
offset += size
|
||||||
|
}
|
||||||
|
// if we ended in the middle of a token, finish it
|
||||||
|
if end-start > 0 {
|
||||||
|
// build token
|
||||||
|
rv = append(rv, &analysis.Token{
|
||||||
|
Term: input[start:end],
|
||||||
|
Start: start,
|
||||||
|
End: end,
|
||||||
|
Position: count + 1,
|
||||||
|
Type: analysis.AlphaNumeric,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
33
vendor/github.com/blevesearch/bleve/analysis/tokenizer/letter/letter.go
generated
vendored
Normal file
33
vendor/github.com/blevesearch/bleve/analysis/tokenizer/letter/letter.go
generated
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright (c) 2016 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package letter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/analysis/tokenizer/character"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "letter"
|
||||||
|
|
||||||
|
func TokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) {
|
||||||
|
return character.NewCharacterTokenizer(unicode.IsLetter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterTokenizer(Name, TokenizerConstructor)
|
||||||
|
}
|
131
vendor/github.com/blevesearch/bleve/analysis/tokenizer/unicode/unicode.go
generated
vendored
Normal file
131
vendor/github.com/blevesearch/bleve/analysis/tokenizer/unicode/unicode.go
generated
vendored
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package unicode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/segment"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "unicode"
|
||||||
|
|
||||||
|
type UnicodeTokenizer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUnicodeTokenizer() *UnicodeTokenizer {
|
||||||
|
return &UnicodeTokenizer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt *UnicodeTokenizer) Tokenize(input []byte) analysis.TokenStream {
|
||||||
|
rvx := make([]analysis.TokenStream, 0, 10) // When rv gets full, append to rvx.
|
||||||
|
rv := make(analysis.TokenStream, 0, 1)
|
||||||
|
|
||||||
|
ta := []analysis.Token(nil)
|
||||||
|
taNext := 0
|
||||||
|
|
||||||
|
segmenter := segment.NewWordSegmenterDirect(input)
|
||||||
|
start := 0
|
||||||
|
pos := 1
|
||||||
|
|
||||||
|
guessRemaining := func(end int) int {
|
||||||
|
avgSegmentLen := end / (len(rv) + 1)
|
||||||
|
if avgSegmentLen < 1 {
|
||||||
|
avgSegmentLen = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingLen := len(input) - end
|
||||||
|
|
||||||
|
return remainingLen / avgSegmentLen
|
||||||
|
}
|
||||||
|
|
||||||
|
for segmenter.Segment() {
|
||||||
|
segmentBytes := segmenter.Bytes()
|
||||||
|
end := start + len(segmentBytes)
|
||||||
|
if segmenter.Type() != segment.None {
|
||||||
|
if taNext >= len(ta) {
|
||||||
|
remainingSegments := guessRemaining(end)
|
||||||
|
if remainingSegments > 1000 {
|
||||||
|
remainingSegments = 1000
|
||||||
|
}
|
||||||
|
if remainingSegments < 1 {
|
||||||
|
remainingSegments = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ta = make([]analysis.Token, remainingSegments)
|
||||||
|
taNext = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &ta[taNext]
|
||||||
|
taNext++
|
||||||
|
|
||||||
|
token.Term = segmentBytes
|
||||||
|
token.Start = start
|
||||||
|
token.End = end
|
||||||
|
token.Position = pos
|
||||||
|
token.Type = convertType(segmenter.Type())
|
||||||
|
|
||||||
|
if len(rv) >= cap(rv) { // When rv is full, save it into rvx.
|
||||||
|
rvx = append(rvx, rv)
|
||||||
|
|
||||||
|
rvCap := cap(rv) * 2
|
||||||
|
if rvCap > 256 {
|
||||||
|
rvCap = 256
|
||||||
|
}
|
||||||
|
|
||||||
|
rv = make(analysis.TokenStream, 0, rvCap) // Next rv cap is bigger.
|
||||||
|
}
|
||||||
|
|
||||||
|
rv = append(rv, token)
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
start = end
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rvx) > 0 {
|
||||||
|
n := len(rv)
|
||||||
|
for _, r := range rvx {
|
||||||
|
n += len(r)
|
||||||
|
}
|
||||||
|
rall := make(analysis.TokenStream, 0, n)
|
||||||
|
for _, r := range rvx {
|
||||||
|
rall = append(rall, r...)
|
||||||
|
}
|
||||||
|
return append(rall, rv...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnicodeTokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) {
|
||||||
|
return NewUnicodeTokenizer(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterTokenizer(Name, UnicodeTokenizerConstructor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertType(segmentWordType int) analysis.TokenType {
|
||||||
|
switch segmentWordType {
|
||||||
|
case segment.Ideo:
|
||||||
|
return analysis.Ideographic
|
||||||
|
case segment.Kana:
|
||||||
|
return analysis.Ideographic
|
||||||
|
case segment.Number:
|
||||||
|
return analysis.Numeric
|
||||||
|
}
|
||||||
|
return analysis.AlphaNumeric
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package analysis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenMap map[string]bool
|
||||||
|
|
||||||
|
func NewTokenMap() TokenMap {
|
||||||
|
return make(TokenMap, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile reads in a list of tokens from a text file,
|
||||||
|
// one per line.
|
||||||
|
// Comments are supported using `#` or `|`
|
||||||
|
func (t TokenMap) LoadFile(filename string) error {
|
||||||
|
data, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return t.LoadBytes(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBytes reads in a list of tokens from memory,
|
||||||
|
// one per line.
|
||||||
|
// Comments are supported using `#` or `|`
|
||||||
|
func (t TokenMap) LoadBytes(data []byte) error {
|
||||||
|
bytesReader := bytes.NewReader(data)
|
||||||
|
bufioReader := bufio.NewReader(bytesReader)
|
||||||
|
line, err := bufioReader.ReadString('\n')
|
||||||
|
for err == nil {
|
||||||
|
t.LoadLine(line)
|
||||||
|
line, err = bufioReader.ReadString('\n')
|
||||||
|
}
|
||||||
|
// if the err was EOF we still need to process the last value
|
||||||
|
if err == io.EOF {
|
||||||
|
t.LoadLine(line)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TokenMap) LoadLine(line string) {
|
||||||
|
// find the start of a comment, if any
|
||||||
|
startComment := strings.IndexAny(line, "#|")
|
||||||
|
if startComment >= 0 {
|
||||||
|
line = line[:startComment]
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := strings.Fields(line)
|
||||||
|
for _, token := range tokens {
|
||||||
|
t.AddToken(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TokenMap) AddToken(token string) {
|
||||||
|
t[token] = true
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package analysis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CharFilter interface {
|
||||||
|
Filter([]byte) []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlphaNumeric TokenType = iota
|
||||||
|
Ideographic
|
||||||
|
Numeric
|
||||||
|
DateTime
|
||||||
|
Shingle
|
||||||
|
Single
|
||||||
|
Double
|
||||||
|
Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
// Token represents one occurrence of a term at a particular location in a
|
||||||
|
// field.
|
||||||
|
type Token struct {
|
||||||
|
// Start specifies the byte offset of the beginning of the term in the
|
||||||
|
// field.
|
||||||
|
Start int `json:"start"`
|
||||||
|
|
||||||
|
// End specifies the byte offset of the end of the term in the field.
|
||||||
|
End int `json:"end"`
|
||||||
|
Term []byte `json:"term"`
|
||||||
|
|
||||||
|
// Position specifies the 1-based index of the token in the sequence of
|
||||||
|
// occurrences of its term in the field.
|
||||||
|
Position int `json:"position"`
|
||||||
|
Type TokenType `json:"type"`
|
||||||
|
KeyWord bool `json:"keyword"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) String() string {
|
||||||
|
return fmt.Sprintf("Start: %d End: %d Position: %d Token: %s Type: %d", t.Start, t.End, t.Position, string(t.Term), t.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenStream []*Token
|
||||||
|
|
||||||
|
// A Tokenizer splits an input string into tokens, the usual behaviour being to
|
||||||
|
// map words to tokens.
|
||||||
|
type Tokenizer interface {
|
||||||
|
Tokenize([]byte) TokenStream
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TokenFilter adds, transforms or removes tokens from a token stream.
|
||||||
|
type TokenFilter interface {
|
||||||
|
Filter(TokenStream) TokenStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type Analyzer struct {
|
||||||
|
CharFilters []CharFilter
|
||||||
|
Tokenizer Tokenizer
|
||||||
|
TokenFilters []TokenFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Analyzer) Analyze(input []byte) TokenStream {
|
||||||
|
if a.CharFilters != nil {
|
||||||
|
for _, cf := range a.CharFilters {
|
||||||
|
input = cf.Filter(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens := a.Tokenizer.Tokenize(input)
|
||||||
|
if a.TokenFilters != nil {
|
||||||
|
for _, tf := range a.TokenFilters {
|
||||||
|
tokens = tf.Filter(tokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrInvalidDateTime = fmt.Errorf("unable to parse datetime with any of the layouts")
|
||||||
|
|
||||||
|
type DateTimeParser interface {
|
||||||
|
ParseDateTime(string) (time.Time, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByteArrayConverter interface {
|
||||||
|
Convert([]byte) (interface{}, error)
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package analysis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteRune(in []rune, pos int) []rune {
|
||||||
|
if pos >= len(in) {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
copy(in[pos:], in[pos+1:])
|
||||||
|
return in[:len(in)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertRune(in []rune, pos int, r rune) []rune {
|
||||||
|
// create a new slice 1 rune larger
|
||||||
|
rv := make([]rune, len(in)+1)
|
||||||
|
// copy the characters before the insert pos
|
||||||
|
copy(rv[0:pos], in[0:pos])
|
||||||
|
// set the inserted rune
|
||||||
|
rv[pos] = r
|
||||||
|
// copy the characters after the insert pos
|
||||||
|
copy(rv[pos+1:], in[pos:])
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildTermFromRunesOptimistic will build a term from the provided runes
|
||||||
|
// AND optimistically attempt to encode into the provided buffer
|
||||||
|
// if at any point it appears the buffer is too small, a new buffer is
|
||||||
|
// allocated and that is used instead
|
||||||
|
// this should be used in cases where frequently the new term is the same
|
||||||
|
// length or shorter than the original term (in number of bytes)
|
||||||
|
func BuildTermFromRunesOptimistic(buf []byte, runes []rune) []byte {
|
||||||
|
rv := buf
|
||||||
|
used := 0
|
||||||
|
for _, r := range runes {
|
||||||
|
nextLen := utf8.RuneLen(r)
|
||||||
|
if used+nextLen > len(rv) {
|
||||||
|
// alloc new buf
|
||||||
|
buf = make([]byte, len(runes)*utf8.UTFMax)
|
||||||
|
// copy work we've already done
|
||||||
|
copy(buf, rv[:used])
|
||||||
|
rv = buf
|
||||||
|
}
|
||||||
|
written := utf8.EncodeRune(rv[used:], r)
|
||||||
|
used += written
|
||||||
|
}
|
||||||
|
return rv[:used]
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildTermFromRunes(runes []rune) []byte {
|
||||||
|
return BuildTermFromRunesOptimistic(make([]byte, len(runes)*utf8.UTFMax), runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TruncateRunes(input []byte, num int) []byte {
|
||||||
|
runes := bytes.Runes(input)
|
||||||
|
runes = runes[:len(runes)-num]
|
||||||
|
out := BuildTermFromRunes(runes)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunesEndsWith(input []rune, suffix string) bool {
|
||||||
|
inputLen := len(input)
|
||||||
|
suffixRunes := []rune(suffix)
|
||||||
|
suffixLen := len(suffixRunes)
|
||||||
|
if suffixLen > inputLen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := suffixLen - 1; i >= 0; i-- {
|
||||||
|
if input[inputLen-(suffixLen-i)] != suffixRunes[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"expvar"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index"
|
||||||
|
"github.com/blevesearch/bleve/index/store/gtreap"
|
||||||
|
"github.com/blevesearch/bleve/index/upsidedown"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
"github.com/blevesearch/bleve/search/highlight/highlighter/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bleveExpVar = expvar.NewMap("bleve")
|
||||||
|
|
||||||
|
type configuration struct {
|
||||||
|
Cache *registry.Cache
|
||||||
|
DefaultHighlighter string
|
||||||
|
DefaultKVStore string
|
||||||
|
DefaultMemKVStore string
|
||||||
|
DefaultIndexType string
|
||||||
|
SlowSearchLogThreshold time.Duration
|
||||||
|
analysisQueue *index.AnalysisQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) SetAnalysisQueueSize(n int) {
|
||||||
|
c.analysisQueue = index.NewAnalysisQueue(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfiguration() *configuration {
|
||||||
|
return &configuration{
|
||||||
|
Cache: registry.NewCache(),
|
||||||
|
analysisQueue: index.NewAnalysisQueue(4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config contains library level configuration
|
||||||
|
var Config *configuration
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
bootStart := time.Now()
|
||||||
|
|
||||||
|
// build the default configuration
|
||||||
|
Config = newConfiguration()
|
||||||
|
|
||||||
|
// set the default highlighter
|
||||||
|
Config.DefaultHighlighter = html.Name
|
||||||
|
|
||||||
|
// default kv store
|
||||||
|
Config.DefaultKVStore = ""
|
||||||
|
|
||||||
|
// default mem only kv store
|
||||||
|
Config.DefaultMemKVStore = gtreap.Name
|
||||||
|
|
||||||
|
// default index
|
||||||
|
Config.DefaultIndexType = upsidedown.Name
|
||||||
|
|
||||||
|
bootDuration := time.Since(bootStart)
|
||||||
|
bleveExpVar.Add("bootDuration", int64(bootDuration))
|
||||||
|
indexStats = NewIndexStats()
|
||||||
|
bleveExpVar.Set("indexes", indexStats)
|
||||||
|
|
||||||
|
initDisk()
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = log.New(ioutil.Discard, "bleve", log.LstdFlags)
|
||||||
|
|
||||||
|
// SetLog sets the logger used for logging
|
||||||
|
// by default log messages are sent to ioutil.Discard
|
||||||
|
func SetLog(l *log.Logger) {
|
||||||
|
logger = l
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// +build appengine appenginevm
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
// in the appengine environment we cannot support disk based indexes
|
||||||
|
// so we do no extra configuration in this method
|
||||||
|
func initDisk() {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// +build !appengine,!appenginevm
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
import "github.com/blevesearch/bleve/index/store/boltdb"
|
||||||
|
|
||||||
|
// in normal environments we configure boltdb as the default storage
|
||||||
|
func initDisk() {
|
||||||
|
// default kv store
|
||||||
|
Config.DefaultKVStore = boltdb.Name
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package bleve is a library for indexing and searching text.
|
||||||
|
|
||||||
|
Example Opening New Index, Indexing Data
|
||||||
|
|
||||||
|
message := struct{
|
||||||
|
Id: "example"
|
||||||
|
From: "marty.schoch@gmail.com",
|
||||||
|
Body: "bleve indexing is easy",
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping := bleve.NewIndexMapping()
|
||||||
|
index, _ := bleve.New("example.bleve", mapping)
|
||||||
|
index.Index(message.Id, message)
|
||||||
|
|
||||||
|
Example Opening Existing Index, Searching Data
|
||||||
|
|
||||||
|
index, _ := bleve.Open("example.bleve")
|
||||||
|
query := bleve.NewQueryStringQuery("bleve")
|
||||||
|
searchRequest := bleve.NewSearchRequest(query)
|
||||||
|
searchResult, _ := index.Search(searchRequest)
|
||||||
|
|
||||||
|
*/
|
||||||
|
package bleve
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package document
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Fields []Field `json:"fields"`
|
||||||
|
CompositeFields []*CompositeField
|
||||||
|
Number uint64 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocument(id string) *Document {
|
||||||
|
return &Document{
|
||||||
|
ID: id,
|
||||||
|
Fields: make([]Field, 0),
|
||||||
|
CompositeFields: make([]*CompositeField, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) AddField(f Field) *Document {
|
||||||
|
switch f := f.(type) {
|
||||||
|
case *CompositeField:
|
||||||
|
d.CompositeFields = append(d.CompositeFields, f)
|
||||||
|
default:
|
||||||
|
d.Fields = append(d.Fields, f)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) GoString() string {
|
||||||
|
fields := ""
|
||||||
|
for i, field := range d.Fields {
|
||||||
|
if i != 0 {
|
||||||
|
fields += ", "
|
||||||
|
}
|
||||||
|
fields += fmt.Sprintf("%#v", field)
|
||||||
|
}
|
||||||
|
compositeFields := ""
|
||||||
|
for i, field := range d.CompositeFields {
|
||||||
|
if i != 0 {
|
||||||
|
compositeFields += ", "
|
||||||
|
}
|
||||||
|
compositeFields += fmt.Sprintf("%#v", field)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("&document.Document{ID:%s, Fields: %s, CompositeFields: %s}", d.ID, fields, compositeFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) NumPlainTextBytes() uint64 {
|
||||||
|
rv := uint64(0)
|
||||||
|
for _, field := range d.Fields {
|
||||||
|
rv += field.NumPlainTextBytes()
|
||||||
|
}
|
||||||
|
for _, compositeField := range d.CompositeFields {
|
||||||
|
for _, field := range d.Fields {
|
||||||
|
if compositeField.includesField(field.Name()) {
|
||||||
|
rv += field.NumPlainTextBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Field interface {
|
||||||
|
// Name returns the path of the field from the root DocumentMapping.
|
||||||
|
// A root field path is "field", a subdocument field is "parent.field".
|
||||||
|
Name() string
|
||||||
|
// ArrayPositions returns the intermediate document and field indices
|
||||||
|
// required to resolve the field value in the document. For example, if the
|
||||||
|
// field path is "doc1.doc2.field" where doc1 and doc2 are slices or
|
||||||
|
// arrays, ArrayPositions returns 2 indices used to resolve "doc2" value in
|
||||||
|
// "doc1", then "field" in "doc2".
|
||||||
|
ArrayPositions() []uint64
|
||||||
|
Options() IndexingOptions
|
||||||
|
Analyze() (int, analysis.TokenFrequencies)
|
||||||
|
Value() []byte
|
||||||
|
|
||||||
|
// NumPlainTextBytes should return the number of plain text bytes
|
||||||
|
// that this field represents - this is a common metric for tracking
|
||||||
|
// the rate of indexing
|
||||||
|
NumPlainTextBytes() uint64
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultBooleanIndexingOptions = StoreField | IndexField
|
||||||
|
|
||||||
|
type BooleanField struct {
|
||||||
|
name string
|
||||||
|
arrayPositions []uint64
|
||||||
|
options IndexingOptions
|
||||||
|
value []byte
|
||||||
|
numPlainTextBytes uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BooleanField) Name() string {
|
||||||
|
return b.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BooleanField) ArrayPositions() []uint64 {
|
||||||
|
return b.arrayPositions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BooleanField) Options() IndexingOptions {
|
||||||
|
return b.options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BooleanField) Analyze() (int, analysis.TokenFrequencies) {
|
||||||
|
tokens := make(analysis.TokenStream, 0)
|
||||||
|
tokens = append(tokens, &analysis.Token{
|
||||||
|
Start: 0,
|
||||||
|
End: len(b.value),
|
||||||
|
Term: b.value,
|
||||||
|
Position: 1,
|
||||||
|
Type: analysis.Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
fieldLength := len(tokens)
|
||||||
|
tokenFreqs := analysis.TokenFrequency(tokens, b.arrayPositions, b.options.IncludeTermVectors())
|
||||||
|
return fieldLength, tokenFreqs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BooleanField) Value() []byte {
|
||||||
|
return b.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BooleanField) Boolean() (bool, error) {
|
||||||
|
if len(b.value) == 1 {
|
||||||
|
return b.value[0] == 'T', nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("boolean field has %d bytes", len(b.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BooleanField) GoString() string {
|
||||||
|
return fmt.Sprintf("&document.BooleanField{Name:%s, Options: %s, Value: %s}", b.name, b.options, b.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BooleanField) NumPlainTextBytes() uint64 {
|
||||||
|
return b.numPlainTextBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBooleanFieldFromBytes(name string, arrayPositions []uint64, value []byte) *BooleanField {
|
||||||
|
return &BooleanField{
|
||||||
|
name: name,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
value: value,
|
||||||
|
options: DefaultNumericIndexingOptions,
|
||||||
|
numPlainTextBytes: uint64(len(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBooleanField(name string, arrayPositions []uint64, b bool) *BooleanField {
|
||||||
|
return NewBooleanFieldWithIndexingOptions(name, arrayPositions, b, DefaultNumericIndexingOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBooleanFieldWithIndexingOptions(name string, arrayPositions []uint64, b bool, options IndexingOptions) *BooleanField {
|
||||||
|
numPlainTextBytes := 5
|
||||||
|
v := []byte("F")
|
||||||
|
if b {
|
||||||
|
numPlainTextBytes = 4
|
||||||
|
v = []byte("T")
|
||||||
|
}
|
||||||
|
return &BooleanField{
|
||||||
|
name: name,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
value: v,
|
||||||
|
options: options,
|
||||||
|
numPlainTextBytes: uint64(numPlainTextBytes),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultCompositeIndexingOptions = IndexField
|
||||||
|
|
||||||
|
type CompositeField struct {
|
||||||
|
name string
|
||||||
|
includedFields map[string]bool
|
||||||
|
excludedFields map[string]bool
|
||||||
|
defaultInclude bool
|
||||||
|
options IndexingOptions
|
||||||
|
totalLength int
|
||||||
|
compositeFrequencies analysis.TokenFrequencies
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompositeField(name string, defaultInclude bool, include []string, exclude []string) *CompositeField {
|
||||||
|
return NewCompositeFieldWithIndexingOptions(name, defaultInclude, include, exclude, DefaultCompositeIndexingOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompositeFieldWithIndexingOptions(name string, defaultInclude bool, include []string, exclude []string, options IndexingOptions) *CompositeField {
|
||||||
|
rv := &CompositeField{
|
||||||
|
name: name,
|
||||||
|
options: options,
|
||||||
|
defaultInclude: defaultInclude,
|
||||||
|
includedFields: make(map[string]bool, len(include)),
|
||||||
|
excludedFields: make(map[string]bool, len(exclude)),
|
||||||
|
compositeFrequencies: make(analysis.TokenFrequencies),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range include {
|
||||||
|
rv.includedFields[i] = true
|
||||||
|
}
|
||||||
|
for _, e := range exclude {
|
||||||
|
rv.excludedFields[e] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompositeField) Name() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompositeField) ArrayPositions() []uint64 {
|
||||||
|
return []uint64{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompositeField) Options() IndexingOptions {
|
||||||
|
return c.options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompositeField) Analyze() (int, analysis.TokenFrequencies) {
|
||||||
|
return c.totalLength, c.compositeFrequencies
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompositeField) Value() []byte {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompositeField) NumPlainTextBytes() uint64 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompositeField) includesField(field string) bool {
|
||||||
|
shouldInclude := c.defaultInclude
|
||||||
|
_, fieldShouldBeIncluded := c.includedFields[field]
|
||||||
|
if fieldShouldBeIncluded {
|
||||||
|
shouldInclude = true
|
||||||
|
}
|
||||||
|
_, fieldShouldBeExcluded := c.excludedFields[field]
|
||||||
|
if fieldShouldBeExcluded {
|
||||||
|
shouldInclude = false
|
||||||
|
}
|
||||||
|
return shouldInclude
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompositeField) Compose(field string, length int, freq analysis.TokenFrequencies) {
|
||||||
|
if c.includesField(field) {
|
||||||
|
c.totalLength += length
|
||||||
|
c.compositeFrequencies.MergeAll(field, freq)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/numeric"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultDateTimeIndexingOptions = StoreField | IndexField
|
||||||
|
const DefaultDateTimePrecisionStep uint = 4
|
||||||
|
|
||||||
|
var MinTimeRepresentable = time.Unix(0, math.MinInt64)
|
||||||
|
var MaxTimeRepresentable = time.Unix(0, math.MaxInt64)
|
||||||
|
|
||||||
|
type DateTimeField struct {
|
||||||
|
name string
|
||||||
|
arrayPositions []uint64
|
||||||
|
options IndexingOptions
|
||||||
|
value numeric.PrefixCoded
|
||||||
|
numPlainTextBytes uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *DateTimeField) Name() string {
|
||||||
|
return n.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *DateTimeField) ArrayPositions() []uint64 {
|
||||||
|
return n.arrayPositions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *DateTimeField) Options() IndexingOptions {
|
||||||
|
return n.options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *DateTimeField) Analyze() (int, analysis.TokenFrequencies) {
|
||||||
|
tokens := make(analysis.TokenStream, 0)
|
||||||
|
tokens = append(tokens, &analysis.Token{
|
||||||
|
Start: 0,
|
||||||
|
End: len(n.value),
|
||||||
|
Term: n.value,
|
||||||
|
Position: 1,
|
||||||
|
Type: analysis.DateTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
original, err := n.value.Int64()
|
||||||
|
if err == nil {
|
||||||
|
|
||||||
|
shift := DefaultDateTimePrecisionStep
|
||||||
|
for shift < 64 {
|
||||||
|
shiftEncoded, err := numeric.NewPrefixCodedInt64(original, shift)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
token := analysis.Token{
|
||||||
|
Start: 0,
|
||||||
|
End: len(shiftEncoded),
|
||||||
|
Term: shiftEncoded,
|
||||||
|
Position: 1,
|
||||||
|
Type: analysis.DateTime,
|
||||||
|
}
|
||||||
|
tokens = append(tokens, &token)
|
||||||
|
shift += DefaultDateTimePrecisionStep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldLength := len(tokens)
|
||||||
|
tokenFreqs := analysis.TokenFrequency(tokens, n.arrayPositions, n.options.IncludeTermVectors())
|
||||||
|
return fieldLength, tokenFreqs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *DateTimeField) Value() []byte {
|
||||||
|
return n.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *DateTimeField) DateTime() (time.Time, error) {
|
||||||
|
i64, err := n.value.Int64()
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return time.Unix(0, i64).UTC(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *DateTimeField) GoString() string {
|
||||||
|
return fmt.Sprintf("&document.DateField{Name:%s, Options: %s, Value: %s}", n.name, n.options, n.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *DateTimeField) NumPlainTextBytes() uint64 {
|
||||||
|
return n.numPlainTextBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDateTimeFieldFromBytes(name string, arrayPositions []uint64, value []byte) *DateTimeField {
|
||||||
|
return &DateTimeField{
|
||||||
|
name: name,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
value: value,
|
||||||
|
options: DefaultDateTimeIndexingOptions,
|
||||||
|
numPlainTextBytes: uint64(len(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDateTimeField(name string, arrayPositions []uint64, dt time.Time) (*DateTimeField, error) {
|
||||||
|
return NewDateTimeFieldWithIndexingOptions(name, arrayPositions, dt, DefaultDateTimeIndexingOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDateTimeFieldWithIndexingOptions(name string, arrayPositions []uint64, dt time.Time, options IndexingOptions) (*DateTimeField, error) {
|
||||||
|
if canRepresent(dt) {
|
||||||
|
dtInt64 := dt.UnixNano()
|
||||||
|
prefixCoded := numeric.MustNewPrefixCodedInt64(dtInt64, 0)
|
||||||
|
return &DateTimeField{
|
||||||
|
name: name,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
value: prefixCoded,
|
||||||
|
options: options,
|
||||||
|
// not correct, just a place holder until we revisit how fields are
|
||||||
|
// represented and can fix this better
|
||||||
|
numPlainTextBytes: uint64(8),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("cannot represent %s in this type", dt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func canRepresent(dt time.Time) bool {
|
||||||
|
if dt.Before(MinTimeRepresentable) || dt.After(MaxTimeRepresentable) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/numeric"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultNumericIndexingOptions = StoreField | IndexField
|
||||||
|
|
||||||
|
const DefaultPrecisionStep uint = 4
|
||||||
|
|
||||||
|
type NumericField struct {
|
||||||
|
name string
|
||||||
|
arrayPositions []uint64
|
||||||
|
options IndexingOptions
|
||||||
|
value numeric.PrefixCoded
|
||||||
|
numPlainTextBytes uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NumericField) Name() string {
|
||||||
|
return n.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NumericField) ArrayPositions() []uint64 {
|
||||||
|
return n.arrayPositions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NumericField) Options() IndexingOptions {
|
||||||
|
return n.options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NumericField) Analyze() (int, analysis.TokenFrequencies) {
|
||||||
|
tokens := make(analysis.TokenStream, 0)
|
||||||
|
tokens = append(tokens, &analysis.Token{
|
||||||
|
Start: 0,
|
||||||
|
End: len(n.value),
|
||||||
|
Term: n.value,
|
||||||
|
Position: 1,
|
||||||
|
Type: analysis.Numeric,
|
||||||
|
})
|
||||||
|
|
||||||
|
original, err := n.value.Int64()
|
||||||
|
if err == nil {
|
||||||
|
|
||||||
|
shift := DefaultPrecisionStep
|
||||||
|
for shift < 64 {
|
||||||
|
shiftEncoded, err := numeric.NewPrefixCodedInt64(original, shift)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
token := analysis.Token{
|
||||||
|
Start: 0,
|
||||||
|
End: len(shiftEncoded),
|
||||||
|
Term: shiftEncoded,
|
||||||
|
Position: 1,
|
||||||
|
Type: analysis.Numeric,
|
||||||
|
}
|
||||||
|
tokens = append(tokens, &token)
|
||||||
|
shift += DefaultPrecisionStep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldLength := len(tokens)
|
||||||
|
tokenFreqs := analysis.TokenFrequency(tokens, n.arrayPositions, n.options.IncludeTermVectors())
|
||||||
|
return fieldLength, tokenFreqs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NumericField) Value() []byte {
|
||||||
|
return n.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NumericField) Number() (float64, error) {
|
||||||
|
i64, err := n.value.Int64()
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, err
|
||||||
|
}
|
||||||
|
return numeric.Int64ToFloat64(i64), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NumericField) GoString() string {
|
||||||
|
return fmt.Sprintf("&document.NumericField{Name:%s, Options: %s, Value: %s}", n.name, n.options, n.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NumericField) NumPlainTextBytes() uint64 {
|
||||||
|
return n.numPlainTextBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNumericFieldFromBytes(name string, arrayPositions []uint64, value []byte) *NumericField {
|
||||||
|
return &NumericField{
|
||||||
|
name: name,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
value: value,
|
||||||
|
options: DefaultNumericIndexingOptions,
|
||||||
|
numPlainTextBytes: uint64(len(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNumericField(name string, arrayPositions []uint64, number float64) *NumericField {
|
||||||
|
return NewNumericFieldWithIndexingOptions(name, arrayPositions, number, DefaultNumericIndexingOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNumericFieldWithIndexingOptions(name string, arrayPositions []uint64, number float64, options IndexingOptions) *NumericField {
|
||||||
|
numberInt64 := numeric.Float64ToInt64(number)
|
||||||
|
prefixCoded := numeric.MustNewPrefixCodedInt64(numberInt64, 0)
|
||||||
|
return &NumericField{
|
||||||
|
name: name,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
value: prefixCoded,
|
||||||
|
options: options,
|
||||||
|
// not correct, just a place holder until we revisit how fields are
|
||||||
|
// represented and can fix this better
|
||||||
|
numPlainTextBytes: uint64(8),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultTextIndexingOptions = IndexField
|
||||||
|
|
||||||
|
type TextField struct {
|
||||||
|
name string
|
||||||
|
arrayPositions []uint64
|
||||||
|
options IndexingOptions
|
||||||
|
analyzer *analysis.Analyzer
|
||||||
|
value []byte
|
||||||
|
numPlainTextBytes uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextField) Name() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextField) ArrayPositions() []uint64 {
|
||||||
|
return t.arrayPositions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextField) Options() IndexingOptions {
|
||||||
|
return t.options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextField) Analyze() (int, analysis.TokenFrequencies) {
|
||||||
|
var tokens analysis.TokenStream
|
||||||
|
if t.analyzer != nil {
|
||||||
|
bytesToAnalyze := t.Value()
|
||||||
|
if t.options.IsStored() {
|
||||||
|
// need to copy
|
||||||
|
bytesCopied := make([]byte, len(bytesToAnalyze))
|
||||||
|
copy(bytesCopied, bytesToAnalyze)
|
||||||
|
bytesToAnalyze = bytesCopied
|
||||||
|
}
|
||||||
|
tokens = t.analyzer.Analyze(bytesToAnalyze)
|
||||||
|
} else {
|
||||||
|
tokens = analysis.TokenStream{
|
||||||
|
&analysis.Token{
|
||||||
|
Start: 0,
|
||||||
|
End: len(t.value),
|
||||||
|
Term: t.value,
|
||||||
|
Position: 1,
|
||||||
|
Type: analysis.AlphaNumeric,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldLength := len(tokens) // number of tokens in this doc field
|
||||||
|
tokenFreqs := analysis.TokenFrequency(tokens, t.arrayPositions, t.options.IncludeTermVectors())
|
||||||
|
return fieldLength, tokenFreqs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextField) Value() []byte {
|
||||||
|
return t.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextField) GoString() string {
|
||||||
|
return fmt.Sprintf("&document.TextField{Name:%s, Options: %s, Analyzer: %v, Value: %s, ArrayPositions: %v}", t.name, t.options, t.analyzer, t.value, t.arrayPositions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextField) NumPlainTextBytes() uint64 {
|
||||||
|
return t.numPlainTextBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTextField(name string, arrayPositions []uint64, value []byte) *TextField {
|
||||||
|
return NewTextFieldWithIndexingOptions(name, arrayPositions, value, DefaultTextIndexingOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTextFieldWithIndexingOptions(name string, arrayPositions []uint64, value []byte, options IndexingOptions) *TextField {
|
||||||
|
return &TextField{
|
||||||
|
name: name,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
options: options,
|
||||||
|
value: value,
|
||||||
|
numPlainTextBytes: uint64(len(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTextFieldWithAnalyzer(name string, arrayPositions []uint64, value []byte, analyzer *analysis.Analyzer) *TextField {
|
||||||
|
return &TextField{
|
||||||
|
name: name,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
options: DefaultTextIndexingOptions,
|
||||||
|
analyzer: analyzer,
|
||||||
|
value: value,
|
||||||
|
numPlainTextBytes: uint64(len(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTextFieldCustom(name string, arrayPositions []uint64, value []byte, options IndexingOptions, analyzer *analysis.Analyzer) *TextField {
|
||||||
|
return &TextField{
|
||||||
|
name: name,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
options: options,
|
||||||
|
analyzer: analyzer,
|
||||||
|
value: value,
|
||||||
|
numPlainTextBytes: uint64(len(value)),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package document
|
||||||
|
|
||||||
|
type IndexingOptions int
|
||||||
|
|
||||||
|
const (
|
||||||
|
IndexField IndexingOptions = 1 << iota
|
||||||
|
StoreField
|
||||||
|
IncludeTermVectors
|
||||||
|
)
|
||||||
|
|
||||||
|
func (o IndexingOptions) IsIndexed() bool {
|
||||||
|
return o&IndexField != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o IndexingOptions) IsStored() bool {
|
||||||
|
return o&StoreField != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o IndexingOptions) IncludeTermVectors() bool {
|
||||||
|
return o&IncludeTermVectors != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o IndexingOptions) String() string {
|
||||||
|
rv := ""
|
||||||
|
if o.IsIndexed() {
|
||||||
|
rv += "INDEXED"
|
||||||
|
}
|
||||||
|
if o.IsStored() {
|
||||||
|
if rv != "" {
|
||||||
|
rv += ", "
|
||||||
|
}
|
||||||
|
rv += "STORE"
|
||||||
|
}
|
||||||
|
if o.IncludeTermVectors() {
|
||||||
|
if rv != "" {
|
||||||
|
rv += ", "
|
||||||
|
}
|
||||||
|
rv += "TV"
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
// Constant Error values which can be compared to determine the type of error
|
||||||
|
const (
|
||||||
|
ErrorIndexPathExists Error = iota
|
||||||
|
ErrorIndexPathDoesNotExist
|
||||||
|
ErrorIndexMetaMissing
|
||||||
|
ErrorIndexMetaCorrupt
|
||||||
|
ErrorUnknownStorageType
|
||||||
|
ErrorIndexClosed
|
||||||
|
ErrorAliasMulti
|
||||||
|
ErrorAliasEmpty
|
||||||
|
ErrorUnknownIndexType
|
||||||
|
ErrorEmptyID
|
||||||
|
ErrorIndexReadInconsistency
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error represents a more strongly typed bleve error for detecting
|
||||||
|
// and handling specific types of errors.
|
||||||
|
type Error int
|
||||||
|
|
||||||
|
func (e Error) Error() string {
|
||||||
|
return errorMessages[e]
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorMessages = map[Error]string{
|
||||||
|
ErrorIndexPathExists: "cannot create new index, path already exists",
|
||||||
|
ErrorIndexPathDoesNotExist: "cannot open index, path does not exist",
|
||||||
|
ErrorIndexMetaMissing: "cannot open index, metadata missing",
|
||||||
|
ErrorIndexMetaCorrupt: "cannot open index, metadata corrupt",
|
||||||
|
ErrorUnknownStorageType: "unknown storage type",
|
||||||
|
ErrorIndexClosed: "index is closed",
|
||||||
|
ErrorAliasMulti: "cannot perform single index operation on multiple index alias",
|
||||||
|
ErrorAliasEmpty: "cannot perform operation on empty alias",
|
||||||
|
ErrorUnknownIndexType: "unknown index type",
|
||||||
|
ErrorEmptyID: "document ID cannot be empty",
|
||||||
|
ErrorIndexReadInconsistency: "index read inconsistency detected",
|
||||||
|
}
|
|
@ -0,0 +1,243 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/document"
|
||||||
|
"github.com/blevesearch/bleve/index"
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
"github.com/blevesearch/bleve/mapping"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Batch groups together multiple Index and Delete
|
||||||
|
// operations you would like performed at the same
|
||||||
|
// time. The Batch structure is NOT thread-safe.
|
||||||
|
// You should only perform operations on a batch
|
||||||
|
// from a single thread at a time. Once batch
|
||||||
|
// execution has started, you may not modify it.
|
||||||
|
type Batch struct {
|
||||||
|
index Index
|
||||||
|
internal *index.Batch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index adds the specified index operation to the
|
||||||
|
// batch. NOTE: the bleve Index is not updated
|
||||||
|
// until the batch is executed.
|
||||||
|
func (b *Batch) Index(id string, data interface{}) error {
|
||||||
|
if id == "" {
|
||||||
|
return ErrorEmptyID
|
||||||
|
}
|
||||||
|
doc := document.NewDocument(id)
|
||||||
|
err := b.index.Mapping().MapDocument(doc, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.internal.Update(doc)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete adds the specified delete operation to the
|
||||||
|
// batch. NOTE: the bleve Index is not updated until
|
||||||
|
// the batch is executed.
|
||||||
|
func (b *Batch) Delete(id string) {
|
||||||
|
if id != "" {
|
||||||
|
b.internal.Delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetInternal adds the specified set internal
|
||||||
|
// operation to the batch. NOTE: the bleve Index is
|
||||||
|
// not updated until the batch is executed.
|
||||||
|
func (b *Batch) SetInternal(key, val []byte) {
|
||||||
|
b.internal.SetInternal(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetInternal adds the specified delete internal
|
||||||
|
// operation to the batch. NOTE: the bleve Index is
|
||||||
|
// not updated until the batch is executed.
|
||||||
|
func (b *Batch) DeleteInternal(key []byte) {
|
||||||
|
b.internal.DeleteInternal(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the total number of operations inside the batch
|
||||||
|
// including normal index operations and internal operations.
|
||||||
|
func (b *Batch) Size() int {
|
||||||
|
return len(b.internal.IndexOps) + len(b.internal.InternalOps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String prints a user friendly string representation of what
|
||||||
|
// is inside this batch.
|
||||||
|
func (b *Batch) String() string {
|
||||||
|
return b.internal.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset returns a Batch to the empty state so that it can
|
||||||
|
// be re-used in the future.
|
||||||
|
func (b *Batch) Reset() {
|
||||||
|
b.internal.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Index implements all the indexing and searching
|
||||||
|
// capabilities of bleve. An Index can be created
|
||||||
|
// using the New() and Open() methods.
|
||||||
|
//
|
||||||
|
// Index() takes an input value, deduces a DocumentMapping for its type,
|
||||||
|
// assigns string paths to its fields or values then applies field mappings on
|
||||||
|
// them.
|
||||||
|
//
|
||||||
|
// The DocumentMapping used to index a value is deduced by the following rules:
|
||||||
|
// 1) If value implements Classifier interface, resolve the mapping from Type().
|
||||||
|
// 2) If value has a string field or value at IndexMapping.TypeField.
|
||||||
|
// (defaulting to "_type"), use it to resolve the mapping. Fields addressing
|
||||||
|
// is described below.
|
||||||
|
// 3) If IndexMapping.DefaultType is registered, return it.
|
||||||
|
// 4) Return IndexMapping.DefaultMapping.
|
||||||
|
//
|
||||||
|
// Each field or nested field of the value is identified by a string path, then
|
||||||
|
// mapped to one or several FieldMappings which extract the result for analysis.
|
||||||
|
//
|
||||||
|
// Struct values fields are identified by their "json:" tag, or by their name.
|
||||||
|
// Nested fields are identified by prefixing with their parent identifier,
|
||||||
|
// separated by a dot.
|
||||||
|
//
|
||||||
|
// Map values entries are identified by their string key. Entries not indexed
|
||||||
|
// by strings are ignored. Entry values are identified recursively like struct
|
||||||
|
// fields.
|
||||||
|
//
|
||||||
|
// Slice and array values are identified by their field name. Their elements
|
||||||
|
// are processed sequentially with the same FieldMapping.
|
||||||
|
//
|
||||||
|
// String, float64 and time.Time values are identified by their field name.
|
||||||
|
// Other types are ignored.
|
||||||
|
//
|
||||||
|
// Each value identifier is decomposed in its parts and recursively address
|
||||||
|
// SubDocumentMappings in the tree starting at the root DocumentMapping. If a
|
||||||
|
// mapping is found, all its FieldMappings are applied to the value. If no
|
||||||
|
// mapping is found and the root DocumentMapping is dynamic, default mappings
|
||||||
|
// are used based on value type and IndexMapping default configurations.
|
||||||
|
//
|
||||||
|
// Finally, mapped values are analyzed, indexed or stored. See
|
||||||
|
// FieldMapping.Analyzer to know how an analyzer is resolved for a given field.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// type Date struct {
|
||||||
|
// Day string `json:"day"`
|
||||||
|
// Month string
|
||||||
|
// Year string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// type Person struct {
|
||||||
|
// FirstName string `json:"first_name"`
|
||||||
|
// LastName string
|
||||||
|
// BirthDate Date `json:"birth_date"`
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// A Person value FirstName is mapped by the SubDocumentMapping at
|
||||||
|
// "first_name". Its LastName is mapped by the one at "LastName". The day of
|
||||||
|
// BirthDate is mapped to the SubDocumentMapping "day" of the root
|
||||||
|
// SubDocumentMapping "birth_date". It will appear as the "birth_date.day"
|
||||||
|
// field in the index. The month is mapped to "birth_date.Month".
|
||||||
|
type Index interface {
|
||||||
|
// Index analyzes, indexes or stores mapped data fields. Supplied
|
||||||
|
// identifier is bound to analyzed data and will be retrieved by search
|
||||||
|
// requests. See Index interface documentation for details about mapping
|
||||||
|
// rules.
|
||||||
|
Index(id string, data interface{}) error
|
||||||
|
Delete(id string) error
|
||||||
|
|
||||||
|
NewBatch() *Batch
|
||||||
|
Batch(b *Batch) error
|
||||||
|
|
||||||
|
// Document returns specified document or nil if the document is not
|
||||||
|
// indexed or stored.
|
||||||
|
Document(id string) (*document.Document, error)
|
||||||
|
// DocCount returns the number of documents in the index.
|
||||||
|
DocCount() (uint64, error)
|
||||||
|
|
||||||
|
Search(req *SearchRequest) (*SearchResult, error)
|
||||||
|
SearchInContext(ctx context.Context, req *SearchRequest) (*SearchResult, error)
|
||||||
|
|
||||||
|
Fields() ([]string, error)
|
||||||
|
|
||||||
|
FieldDict(field string) (index.FieldDict, error)
|
||||||
|
FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error)
|
||||||
|
FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error)
|
||||||
|
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
Mapping() mapping.IndexMapping
|
||||||
|
|
||||||
|
Stats() *IndexStat
|
||||||
|
StatsMap() map[string]interface{}
|
||||||
|
|
||||||
|
GetInternal(key []byte) ([]byte, error)
|
||||||
|
SetInternal(key, val []byte) error
|
||||||
|
DeleteInternal(key []byte) error
|
||||||
|
|
||||||
|
// Name returns the name of the index (by default this is the path)
|
||||||
|
Name() string
|
||||||
|
// SetName lets you assign your own logical name to this index
|
||||||
|
SetName(string)
|
||||||
|
|
||||||
|
// Advanced returns the indexer and data store, exposing lower level
|
||||||
|
// methods to enumerate records and access data.
|
||||||
|
Advanced() (index.Index, store.KVStore, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New index at the specified path, must not exist.
|
||||||
|
// The provided mapping will be used for all
|
||||||
|
// Index/Search operations.
|
||||||
|
func New(path string, mapping mapping.IndexMapping) (Index, error) {
|
||||||
|
return newIndexUsing(path, mapping, Config.DefaultIndexType, Config.DefaultKVStore, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemOnly creates a memory-only index.
|
||||||
|
// The contents of the index is NOT persisted,
|
||||||
|
// and will be lost once closed.
|
||||||
|
// The provided mapping will be used for all
|
||||||
|
// Index/Search operations.
|
||||||
|
func NewMemOnly(mapping mapping.IndexMapping) (Index, error) {
|
||||||
|
return newIndexUsing("", mapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUsing creates index at the specified path,
|
||||||
|
// which must not already exist.
|
||||||
|
// The provided mapping will be used for all
|
||||||
|
// Index/Search operations.
|
||||||
|
// The specified index type will be used.
|
||||||
|
// The specified kvstore implementation will be used
|
||||||
|
// and the provided kvconfig will be passed to its
|
||||||
|
// constructor. Note that currently the values of kvconfig must
|
||||||
|
// be able to be marshaled and unmarshaled using the encoding/json library (used
|
||||||
|
// when reading/writing the index metadata file).
|
||||||
|
func NewUsing(path string, mapping mapping.IndexMapping, indexType string, kvstore string, kvconfig map[string]interface{}) (Index, error) {
|
||||||
|
return newIndexUsing(path, mapping, indexType, kvstore, kvconfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open index at the specified path, must exist.
|
||||||
|
// The mapping used when it was created will be used for all Index/Search operations.
|
||||||
|
func Open(path string) (Index, error) {
|
||||||
|
return openIndexUsing(path, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenUsing opens index at the specified path, must exist.
|
||||||
|
// The mapping used when it was created will be used for all Index/Search operations.
|
||||||
|
// The provided runtimeConfig can override settings
|
||||||
|
// persisted when the kvstore was created.
|
||||||
|
func OpenUsing(path string, runtimeConfig map[string]interface{}) (Index, error) {
|
||||||
|
return openIndexUsing(path, runtimeConfig)
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright (c) 2015 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package index
|
||||||
|
|
||||||
|
import "github.com/blevesearch/bleve/document"
|
||||||
|
|
||||||
|
type IndexRow interface {
|
||||||
|
KeySize() int
|
||||||
|
KeyTo([]byte) (int, error)
|
||||||
|
Key() []byte
|
||||||
|
|
||||||
|
ValueSize() int
|
||||||
|
ValueTo([]byte) (int, error)
|
||||||
|
Value() []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalysisResult struct {
|
||||||
|
DocID string
|
||||||
|
Rows []IndexRow
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalysisWork struct {
|
||||||
|
i Index
|
||||||
|
d *document.Document
|
||||||
|
rc chan *AnalysisResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnalysisWork(i Index, d *document.Document, rc chan *AnalysisResult) *AnalysisWork {
|
||||||
|
return &AnalysisWork{
|
||||||
|
i: i,
|
||||||
|
d: d,
|
||||||
|
rc: rc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalysisQueue struct {
|
||||||
|
queue chan *AnalysisWork
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *AnalysisQueue) Queue(work *AnalysisWork) {
|
||||||
|
q.queue <- work
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *AnalysisQueue) Close() {
|
||||||
|
close(q.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnalysisQueue(numWorkers int) *AnalysisQueue {
|
||||||
|
rv := AnalysisQueue{
|
||||||
|
queue: make(chan *AnalysisWork),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
go AnalysisWorker(rv)
|
||||||
|
}
|
||||||
|
return &rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalysisWorker(q AnalysisQueue) {
|
||||||
|
// read work off the queue
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-q.done:
|
||||||
|
return
|
||||||
|
case w := <-q.queue:
|
||||||
|
r := w.i.Analyze(w.d)
|
||||||
|
w.rc <- r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright (c) 2015 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FieldCache struct {
|
||||||
|
fieldIndexes map[string]uint16
|
||||||
|
indexFields []string
|
||||||
|
lastFieldIndex int
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFieldCache() *FieldCache {
|
||||||
|
return &FieldCache{
|
||||||
|
fieldIndexes: make(map[string]uint16),
|
||||||
|
lastFieldIndex: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldCache) AddExisting(field string, index uint16) {
|
||||||
|
f.mutex.Lock()
|
||||||
|
f.addLOCKED(field, index)
|
||||||
|
f.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldCache) addLOCKED(field string, index uint16) uint16 {
|
||||||
|
f.fieldIndexes[field] = index
|
||||||
|
if len(f.indexFields) < int(index)+1 {
|
||||||
|
prevIndexFields := f.indexFields
|
||||||
|
f.indexFields = make([]string, int(index)+16)
|
||||||
|
copy(f.indexFields, prevIndexFields)
|
||||||
|
}
|
||||||
|
f.indexFields[int(index)] = field
|
||||||
|
if int(index) > f.lastFieldIndex {
|
||||||
|
f.lastFieldIndex = int(index)
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldNamed returns the index of the field, and whether or not it existed
|
||||||
|
// before this call. if createIfMissing is true, and new field index is assigned
|
||||||
|
// but the second return value will still be false
|
||||||
|
func (f *FieldCache) FieldNamed(field string, createIfMissing bool) (uint16, bool) {
|
||||||
|
f.mutex.RLock()
|
||||||
|
if index, ok := f.fieldIndexes[field]; ok {
|
||||||
|
f.mutex.RUnlock()
|
||||||
|
return index, true
|
||||||
|
} else if !createIfMissing {
|
||||||
|
f.mutex.RUnlock()
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
// trade read lock for write lock
|
||||||
|
f.mutex.RUnlock()
|
||||||
|
f.mutex.Lock()
|
||||||
|
// need to check again with write lock
|
||||||
|
if index, ok := f.fieldIndexes[field]; ok {
|
||||||
|
f.mutex.Unlock()
|
||||||
|
return index, true
|
||||||
|
}
|
||||||
|
// assign next field id
|
||||||
|
index := f.addLOCKED(field, uint16(f.lastFieldIndex+1))
|
||||||
|
f.mutex.Unlock()
|
||||||
|
return index, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldCache) FieldIndexed(index uint16) (field string) {
|
||||||
|
f.mutex.RLock()
|
||||||
|
if int(index) < len(f.indexFields) {
|
||||||
|
field = f.indexFields[int(index)]
|
||||||
|
}
|
||||||
|
f.mutex.RUnlock()
|
||||||
|
return field
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/document"
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrorUnknownStorageType = fmt.Errorf("unknown storage type")
|
||||||
|
|
||||||
|
type Index interface {
|
||||||
|
Open() error
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
Update(doc *document.Document) error
|
||||||
|
Delete(id string) error
|
||||||
|
Batch(batch *Batch) error
|
||||||
|
|
||||||
|
SetInternal(key, val []byte) error
|
||||||
|
DeleteInternal(key []byte) error
|
||||||
|
|
||||||
|
// Reader returns a low-level accessor on the index data. Close it to
|
||||||
|
// release associated resources.
|
||||||
|
Reader() (IndexReader, error)
|
||||||
|
|
||||||
|
Stats() json.Marshaler
|
||||||
|
StatsMap() map[string]interface{}
|
||||||
|
|
||||||
|
Analyze(d *document.Document) *AnalysisResult
|
||||||
|
|
||||||
|
Advanced() (store.KVStore, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexReader interface {
|
||||||
|
TermFieldReader(term []byte, field string, includeFreq, includeNorm, includeTermVectors bool) (TermFieldReader, error)
|
||||||
|
|
||||||
|
// DocIDReader returns an iterator over all doc ids
|
||||||
|
// The caller must close returned instance to release associated resources.
|
||||||
|
DocIDReaderAll() (DocIDReader, error)
|
||||||
|
|
||||||
|
DocIDReaderOnly(ids []string) (DocIDReader, error)
|
||||||
|
|
||||||
|
FieldDict(field string) (FieldDict, error)
|
||||||
|
|
||||||
|
// FieldDictRange is currently defined to include the start and end terms
|
||||||
|
FieldDictRange(field string, startTerm []byte, endTerm []byte) (FieldDict, error)
|
||||||
|
FieldDictPrefix(field string, termPrefix []byte) (FieldDict, error)
|
||||||
|
|
||||||
|
Document(id string) (*document.Document, error)
|
||||||
|
DocumentFieldTerms(id IndexInternalID, fields []string) (FieldTerms, error)
|
||||||
|
|
||||||
|
Fields() ([]string, error)
|
||||||
|
|
||||||
|
GetInternal(key []byte) ([]byte, error)
|
||||||
|
|
||||||
|
DocCount() (uint64, error)
|
||||||
|
|
||||||
|
ExternalID(id IndexInternalID) (string, error)
|
||||||
|
InternalID(id string) (IndexInternalID, error)
|
||||||
|
|
||||||
|
DumpAll() chan interface{}
|
||||||
|
DumpDoc(id string) chan interface{}
|
||||||
|
DumpFields() chan interface{}
|
||||||
|
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldTerms contains the terms used by a document, keyed by field
|
||||||
|
type FieldTerms map[string][]string
|
||||||
|
|
||||||
|
// FieldsNotYetCached returns a list of fields not yet cached out of a larger list of fields
|
||||||
|
func (f FieldTerms) FieldsNotYetCached(fields []string) []string {
|
||||||
|
rv := make([]string, 0, len(fields))
|
||||||
|
for _, field := range fields {
|
||||||
|
if _, ok := f[field]; !ok {
|
||||||
|
rv = append(rv, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge will combine two FieldTerms
|
||||||
|
// it assumes that the terms lists are complete (thus do not need to be merged)
|
||||||
|
// field terms from the other list always replace the ones in the receiver
|
||||||
|
func (f FieldTerms) Merge(other FieldTerms) {
|
||||||
|
for field, terms := range other {
|
||||||
|
f[field] = terms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TermFieldVector struct {
|
||||||
|
Field string
|
||||||
|
ArrayPositions []uint64
|
||||||
|
Pos uint64
|
||||||
|
Start uint64
|
||||||
|
End uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexInternalID is an opaque document identifier interal to the index impl
|
||||||
|
type IndexInternalID []byte
|
||||||
|
|
||||||
|
func (id IndexInternalID) Equals(other IndexInternalID) bool {
|
||||||
|
return id.Compare(other) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id IndexInternalID) Compare(other IndexInternalID) int {
|
||||||
|
return bytes.Compare(id, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TermFieldDoc struct {
|
||||||
|
Term string
|
||||||
|
ID IndexInternalID
|
||||||
|
Freq uint64
|
||||||
|
Norm float64
|
||||||
|
Vectors []*TermFieldVector
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset allows an already allocated TermFieldDoc to be reused
|
||||||
|
func (tfd *TermFieldDoc) Reset() *TermFieldDoc {
|
||||||
|
// remember the []byte used for the ID
|
||||||
|
id := tfd.ID
|
||||||
|
// idiom to copy over from empty TermFieldDoc (0 allocations)
|
||||||
|
*tfd = TermFieldDoc{}
|
||||||
|
// reuse the []byte already allocated (and reset len to 0)
|
||||||
|
tfd.ID = id[:0]
|
||||||
|
return tfd
|
||||||
|
}
|
||||||
|
|
||||||
|
// TermFieldReader is the interface exposing the enumeration of documents
|
||||||
|
// containing a given term in a given field. Documents are returned in byte
|
||||||
|
// lexicographic order over their identifiers.
|
||||||
|
type TermFieldReader interface {
|
||||||
|
// Next returns the next document containing the term in this field, or nil
|
||||||
|
// when it reaches the end of the enumeration. The preAlloced TermFieldDoc
|
||||||
|
// is optional, and when non-nil, will be used instead of allocating memory.
|
||||||
|
Next(preAlloced *TermFieldDoc) (*TermFieldDoc, error)
|
||||||
|
|
||||||
|
// Advance resets the enumeration at specified document or its immediate
|
||||||
|
// follower.
|
||||||
|
Advance(ID IndexInternalID, preAlloced *TermFieldDoc) (*TermFieldDoc, error)
|
||||||
|
|
||||||
|
// Count returns the number of documents contains the term in this field.
|
||||||
|
Count() uint64
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DictEntry struct {
|
||||||
|
Term string
|
||||||
|
Count uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldDict interface {
|
||||||
|
Next() (*DictEntry, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocIDReader is the interface exposing enumeration of documents identifiers.
|
||||||
|
// Close the reader to release associated resources.
|
||||||
|
type DocIDReader interface {
|
||||||
|
// Next returns the next document internal identifier in the natural
|
||||||
|
// index order, nil when the end of the sequence is reached.
|
||||||
|
Next() (IndexInternalID, error)
|
||||||
|
|
||||||
|
// Advance resets the iteration to the first internal identifier greater than
|
||||||
|
// or equal to ID. If ID is smaller than the start of the range, the iteration
|
||||||
|
// will start there instead. If ID is greater than or equal to the end of
|
||||||
|
// the range, Next() call will return io.EOF.
|
||||||
|
Advance(ID IndexInternalID) (IndexInternalID, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Batch struct {
|
||||||
|
IndexOps map[string]*document.Document
|
||||||
|
InternalOps map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBatch() *Batch {
|
||||||
|
return &Batch{
|
||||||
|
IndexOps: make(map[string]*document.Document),
|
||||||
|
InternalOps: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) Update(doc *document.Document) {
|
||||||
|
b.IndexOps[doc.ID] = doc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) Delete(id string) {
|
||||||
|
b.IndexOps[id] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) SetInternal(key, val []byte) {
|
||||||
|
b.InternalOps[string(key)] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) DeleteInternal(key []byte) {
|
||||||
|
b.InternalOps[string(key)] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) String() string {
|
||||||
|
rv := fmt.Sprintf("Batch (%d ops, %d internal ops)\n", len(b.IndexOps), len(b.InternalOps))
|
||||||
|
for k, v := range b.IndexOps {
|
||||||
|
if v != nil {
|
||||||
|
rv += fmt.Sprintf("\tINDEX - '%s'\n", k)
|
||||||
|
} else {
|
||||||
|
rv += fmt.Sprintf("\tDELETE - '%s'\n", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range b.InternalOps {
|
||||||
|
if v != nil {
|
||||||
|
rv += fmt.Sprintf("\tSET INTERNAL - '%s'\n", k)
|
||||||
|
} else {
|
||||||
|
rv += fmt.Sprintf("\tDELETE INTERNAL - '%s'\n", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) Reset() {
|
||||||
|
b.IndexOps = make(map[string]*document.Document)
|
||||||
|
b.InternalOps = make(map[string][]byte)
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
type op struct {
|
||||||
|
K []byte
|
||||||
|
V []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmulatedBatch struct {
|
||||||
|
Ops []*op
|
||||||
|
Merger *EmulatedMerge
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmulatedBatch(mo MergeOperator) *EmulatedBatch {
|
||||||
|
return &EmulatedBatch{
|
||||||
|
Ops: make([]*op, 0, 1000),
|
||||||
|
Merger: NewEmulatedMerge(mo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *EmulatedBatch) Set(key, val []byte) {
|
||||||
|
ck := make([]byte, len(key))
|
||||||
|
copy(ck, key)
|
||||||
|
cv := make([]byte, len(val))
|
||||||
|
copy(cv, val)
|
||||||
|
b.Ops = append(b.Ops, &op{ck, cv})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *EmulatedBatch) Delete(key []byte) {
|
||||||
|
ck := make([]byte, len(key))
|
||||||
|
copy(ck, key)
|
||||||
|
b.Ops = append(b.Ops, &op{ck, nil})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *EmulatedBatch) Merge(key, val []byte) {
|
||||||
|
ck := make([]byte, len(key))
|
||||||
|
copy(ck, key)
|
||||||
|
cv := make([]byte, len(val))
|
||||||
|
copy(cv, val)
|
||||||
|
b.Merger.Merge(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *EmulatedBatch) Reset() {
|
||||||
|
b.Ops = b.Ops[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *EmulatedBatch) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
85
vendor/github.com/blevesearch/bleve/index/store/boltdb/iterator.go
generated
vendored
Normal file
85
vendor/github.com/blevesearch/bleve/index/store/boltdb/iterator.go
generated
vendored
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package boltdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Iterator struct {
|
||||||
|
store *Store
|
||||||
|
tx *bolt.Tx
|
||||||
|
cursor *bolt.Cursor
|
||||||
|
prefix []byte
|
||||||
|
start []byte
|
||||||
|
end []byte
|
||||||
|
valid bool
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) updateValid() {
|
||||||
|
i.valid = (i.key != nil)
|
||||||
|
if i.valid {
|
||||||
|
if i.prefix != nil {
|
||||||
|
i.valid = bytes.HasPrefix(i.key, i.prefix)
|
||||||
|
} else if i.end != nil {
|
||||||
|
i.valid = bytes.Compare(i.key, i.end) < 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Seek(k []byte) {
|
||||||
|
if i.start != nil && bytes.Compare(k, i.start) < 0 {
|
||||||
|
k = i.start
|
||||||
|
}
|
||||||
|
if i.prefix != nil && !bytes.HasPrefix(k, i.prefix) {
|
||||||
|
if bytes.Compare(k, i.prefix) < 0 {
|
||||||
|
k = i.prefix
|
||||||
|
} else {
|
||||||
|
i.valid = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i.key, i.val = i.cursor.Seek(k)
|
||||||
|
i.updateValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Next() {
|
||||||
|
i.key, i.val = i.cursor.Next()
|
||||||
|
i.updateValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Current() ([]byte, []byte, bool) {
|
||||||
|
return i.key, i.val, i.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Key() []byte {
|
||||||
|
return i.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Value() []byte {
|
||||||
|
return i.val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Valid() bool {
|
||||||
|
return i.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package boltdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reader struct {
|
||||||
|
store *Store
|
||||||
|
tx *bolt.Tx
|
||||||
|
bucket *bolt.Bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) Get(key []byte) ([]byte, error) {
|
||||||
|
var rv []byte
|
||||||
|
v := r.bucket.Get(key)
|
||||||
|
if v != nil {
|
||||||
|
rv = make([]byte, len(v))
|
||||||
|
copy(rv, v)
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) MultiGet(keys [][]byte) ([][]byte, error) {
|
||||||
|
return store.MultiGet(r, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) PrefixIterator(prefix []byte) store.KVIterator {
|
||||||
|
cursor := r.bucket.Cursor()
|
||||||
|
|
||||||
|
rv := &Iterator{
|
||||||
|
store: r.store,
|
||||||
|
tx: r.tx,
|
||||||
|
cursor: cursor,
|
||||||
|
prefix: prefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.Seek(prefix)
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) RangeIterator(start, end []byte) store.KVIterator {
|
||||||
|
cursor := r.bucket.Cursor()
|
||||||
|
|
||||||
|
rv := &Iterator{
|
||||||
|
store: r.store,
|
||||||
|
tx: r.tx,
|
||||||
|
cursor: cursor,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.Seek(start)
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) Close() error {
|
||||||
|
return r.tx.Rollback()
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package boltdb
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type stats struct {
|
||||||
|
s *Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stats) MarshalJSON() ([]byte, error) {
|
||||||
|
bs := s.s.db.Stats()
|
||||||
|
return json.Marshal(bs)
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package boltdb implements a store.KVStore on top of BoltDB. It supports the
|
||||||
|
// following options:
|
||||||
|
//
|
||||||
|
// "bucket" (string): the name of BoltDB bucket to use, defaults to "bleve".
|
||||||
|
//
|
||||||
|
// "nosync" (bool): if true, set boltdb.DB.NoSync to true. It speeds up index
|
||||||
|
// operations in exchange of losing integrity guarantees if indexation aborts
|
||||||
|
// without closing the index. Use it when rebuilding indexes from zero.
|
||||||
|
package boltdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Name = "boltdb"
|
||||||
|
defaultCompactBatchSize = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
path string
|
||||||
|
bucket string
|
||||||
|
db *bolt.DB
|
||||||
|
noSync bool
|
||||||
|
fillPercent float64
|
||||||
|
mo store.MergeOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(mo store.MergeOperator, config map[string]interface{}) (store.KVStore, error) {
|
||||||
|
path, ok := config["path"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("must specify path")
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket, ok := config["bucket"].(string)
|
||||||
|
if !ok {
|
||||||
|
bucket = "bleve"
|
||||||
|
}
|
||||||
|
|
||||||
|
noSync, _ := config["nosync"].(bool)
|
||||||
|
|
||||||
|
fillPercent, ok := config["fillPercent"].(float64)
|
||||||
|
if !ok {
|
||||||
|
fillPercent = bolt.DefaultFillPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
bo := &bolt.Options{}
|
||||||
|
ro, ok := config["read_only"].(bool)
|
||||||
|
if ok {
|
||||||
|
bo.ReadOnly = ro
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := bolt.Open(path, 0600, bo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db.NoSync = noSync
|
||||||
|
|
||||||
|
if !bo.ReadOnly {
|
||||||
|
err = db.Update(func(tx *bolt.Tx) error {
|
||||||
|
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := Store{
|
||||||
|
path: path,
|
||||||
|
bucket: bucket,
|
||||||
|
db: db,
|
||||||
|
mo: mo,
|
||||||
|
noSync: noSync,
|
||||||
|
fillPercent: fillPercent,
|
||||||
|
}
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *Store) Close() error {
|
||||||
|
return bs.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *Store) Reader() (store.KVReader, error) {
|
||||||
|
tx, err := bs.db.Begin(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Reader{
|
||||||
|
store: bs,
|
||||||
|
tx: tx,
|
||||||
|
bucket: tx.Bucket([]byte(bs.bucket)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *Store) Writer() (store.KVWriter, error) {
|
||||||
|
return &Writer{
|
||||||
|
store: bs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *Store) Stats() json.Marshaler {
|
||||||
|
return &stats{
|
||||||
|
s: bs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompactWithBatchSize removes DictionaryTerm entries with a count of zero (in batchSize batches)
|
||||||
|
// Removing entries is a workaround for github issue #374.
|
||||||
|
func (bs *Store) CompactWithBatchSize(batchSize int) error {
|
||||||
|
for {
|
||||||
|
cnt := 0
|
||||||
|
err := bs.db.Batch(func(tx *bolt.Tx) error {
|
||||||
|
c := tx.Bucket([]byte(bs.bucket)).Cursor()
|
||||||
|
prefix := []byte("d")
|
||||||
|
|
||||||
|
for k, v := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, v = c.Next() {
|
||||||
|
if bytes.Equal(v, []byte{0}) {
|
||||||
|
cnt++
|
||||||
|
if err := c.Delete(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cnt == batchSize {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cnt == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact calls CompactWithBatchSize with a default batch size of 100. This is a workaround
|
||||||
|
// for github issue #374.
|
||||||
|
func (bs *Store) Compact() error {
|
||||||
|
return bs.CompactWithBatchSize(defaultCompactBatchSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterKVStore(Name, New)
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package boltdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Writer struct {
|
||||||
|
store *Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) NewBatch() store.KVBatch {
|
||||||
|
return store.NewEmulatedBatch(w.store.mo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) NewBatchEx(options store.KVBatchOptions) ([]byte, store.KVBatch, error) {
|
||||||
|
return make([]byte, options.TotalBytes), w.NewBatch(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) ExecuteBatch(batch store.KVBatch) (err error) {
|
||||||
|
|
||||||
|
emulatedBatch, ok := batch.(*store.EmulatedBatch)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("wrong type of batch")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := w.store.db.Begin(true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// defer function to ensure that once started,
|
||||||
|
// we either Commit tx or Rollback
|
||||||
|
defer func() {
|
||||||
|
// if nothing went wrong, commit
|
||||||
|
if err == nil {
|
||||||
|
// careful to catch error here too
|
||||||
|
err = tx.Commit()
|
||||||
|
} else {
|
||||||
|
// caller should see error that caused abort,
|
||||||
|
// not success or failure of Rollback itself
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bucket := tx.Bucket([]byte(w.store.bucket))
|
||||||
|
bucket.FillPercent = w.store.fillPercent
|
||||||
|
|
||||||
|
for k, mergeOps := range emulatedBatch.Merger.Merges {
|
||||||
|
kb := []byte(k)
|
||||||
|
existingVal := bucket.Get(kb)
|
||||||
|
mergedVal, fullMergeOk := w.store.mo.FullMerge(kb, existingVal, mergeOps)
|
||||||
|
if !fullMergeOk {
|
||||||
|
err = fmt.Errorf("merge operator returned failure")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = bucket.Put(kb, mergedVal)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, op := range emulatedBatch.Ops {
|
||||||
|
if op.V != nil {
|
||||||
|
err = bucket.Put(op.K, op.V)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = bucket.Delete(op.K)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
152
vendor/github.com/blevesearch/bleve/index/store/gtreap/iterator.go
generated
vendored
Normal file
152
vendor/github.com/blevesearch/bleve/index/store/gtreap/iterator.go
generated
vendored
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
// Copyright (c) 2015 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package gtreap provides an in-memory implementation of the
|
||||||
|
// KVStore interfaces using the gtreap balanced-binary treap,
|
||||||
|
// copy-on-write data structure.
|
||||||
|
package gtreap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/steveyen/gtreap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Iterator struct {
|
||||||
|
t *gtreap.Treap
|
||||||
|
|
||||||
|
m sync.Mutex
|
||||||
|
cancelCh chan struct{}
|
||||||
|
nextCh chan *Item
|
||||||
|
curr *Item
|
||||||
|
currOk bool
|
||||||
|
|
||||||
|
prefix []byte
|
||||||
|
start []byte
|
||||||
|
end []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Iterator) Seek(k []byte) {
|
||||||
|
if w.start != nil && bytes.Compare(k, w.start) < 0 {
|
||||||
|
k = w.start
|
||||||
|
}
|
||||||
|
if w.prefix != nil && !bytes.HasPrefix(k, w.prefix) {
|
||||||
|
if bytes.Compare(k, w.prefix) < 0 {
|
||||||
|
k = w.prefix
|
||||||
|
} else {
|
||||||
|
var end []byte
|
||||||
|
for i := len(w.prefix) - 1; i >= 0; i-- {
|
||||||
|
c := w.prefix[i]
|
||||||
|
if c < 0xff {
|
||||||
|
end = make([]byte, i+1)
|
||||||
|
copy(end, w.prefix)
|
||||||
|
end[i] = c + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.restart(&Item{k: k})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Iterator) restart(start *Item) *Iterator {
|
||||||
|
cancelCh := make(chan struct{})
|
||||||
|
nextCh := make(chan *Item, 1)
|
||||||
|
|
||||||
|
w.m.Lock()
|
||||||
|
if w.cancelCh != nil {
|
||||||
|
close(w.cancelCh)
|
||||||
|
}
|
||||||
|
w.cancelCh = cancelCh
|
||||||
|
w.nextCh = nextCh
|
||||||
|
w.curr = nil
|
||||||
|
w.currOk = false
|
||||||
|
w.m.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if start != nil {
|
||||||
|
w.t.VisitAscend(start, func(itm gtreap.Item) bool {
|
||||||
|
select {
|
||||||
|
case <-cancelCh:
|
||||||
|
return false
|
||||||
|
case nextCh <- itm.(*Item):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
close(nextCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
w.Next()
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Iterator) Next() {
|
||||||
|
w.m.Lock()
|
||||||
|
nextCh := w.nextCh
|
||||||
|
w.m.Unlock()
|
||||||
|
w.curr, w.currOk = <-nextCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Iterator) Current() ([]byte, []byte, bool) {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
|
if !w.currOk || w.curr == nil {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
if w.prefix != nil && !bytes.HasPrefix(w.curr.k, w.prefix) {
|
||||||
|
return nil, nil, false
|
||||||
|
} else if w.end != nil && bytes.Compare(w.curr.k, w.end) >= 0 {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
return w.curr.k, w.curr.v, w.currOk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Iterator) Key() []byte {
|
||||||
|
k, _, ok := w.Current()
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Iterator) Value() []byte {
|
||||||
|
_, v, ok := w.Current()
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Iterator) Valid() bool {
|
||||||
|
_, _, ok := w.Current()
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Iterator) Close() error {
|
||||||
|
w.m.Lock()
|
||||||
|
if w.cancelCh != nil {
|
||||||
|
close(w.cancelCh)
|
||||||
|
}
|
||||||
|
w.cancelCh = nil
|
||||||
|
w.nextCh = nil
|
||||||
|
w.curr = nil
|
||||||
|
w.currOk = false
|
||||||
|
w.m.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright (c) 2015 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package gtreap provides an in-memory implementation of the
|
||||||
|
// KVStore interfaces using the gtreap balanced-binary treap,
|
||||||
|
// copy-on-write data structure.
|
||||||
|
package gtreap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
|
||||||
|
"github.com/steveyen/gtreap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reader struct {
|
||||||
|
t *gtreap.Treap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Reader) Get(k []byte) (v []byte, err error) {
|
||||||
|
var rv []byte
|
||||||
|
itm := w.t.Get(&Item{k: k})
|
||||||
|
if itm != nil {
|
||||||
|
rv = make([]byte, len(itm.(*Item).v))
|
||||||
|
copy(rv, itm.(*Item).v)
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) MultiGet(keys [][]byte) ([][]byte, error) {
|
||||||
|
return store.MultiGet(r, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Reader) PrefixIterator(k []byte) store.KVIterator {
|
||||||
|
rv := Iterator{
|
||||||
|
t: w.t,
|
||||||
|
prefix: k,
|
||||||
|
}
|
||||||
|
rv.restart(&Item{k: k})
|
||||||
|
return &rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Reader) RangeIterator(start, end []byte) store.KVIterator {
|
||||||
|
rv := Iterator{
|
||||||
|
t: w.t,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
}
|
||||||
|
rv.restart(&Item{k: start})
|
||||||
|
return &rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Reader) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright (c) 2015 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package gtreap provides an in-memory implementation of the
|
||||||
|
// KVStore interfaces using the gtreap balanced-binary treap,
|
||||||
|
// copy-on-write data structure.
|
||||||
|
|
||||||
|
package gtreap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
"github.com/steveyen/gtreap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "gtreap"
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
m sync.Mutex
|
||||||
|
t *gtreap.Treap
|
||||||
|
mo store.MergeOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
k []byte
|
||||||
|
v []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func itemCompare(a, b interface{}) int {
|
||||||
|
return bytes.Compare(a.(*Item).k, b.(*Item).k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(mo store.MergeOperator, config map[string]interface{}) (store.KVStore, error) {
|
||||||
|
path, ok := config["path"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("must specify path")
|
||||||
|
}
|
||||||
|
if path != "" {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := Store{
|
||||||
|
t: gtreap.NewTreap(itemCompare),
|
||||||
|
mo: mo,
|
||||||
|
}
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Reader() (store.KVReader, error) {
|
||||||
|
s.m.Lock()
|
||||||
|
t := s.t
|
||||||
|
s.m.Unlock()
|
||||||
|
return &Reader{t: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Writer() (store.KVWriter, error) {
|
||||||
|
return &Writer{s: s}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterKVStore(Name, New)
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright (c) 2015 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package gtreap provides an in-memory implementation of the
|
||||||
|
// KVStore interfaces using the gtreap balanced-binary treap,
|
||||||
|
// copy-on-write data structure.
|
||||||
|
package gtreap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Writer struct {
|
||||||
|
s *Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) NewBatch() store.KVBatch {
|
||||||
|
return store.NewEmulatedBatch(w.s.mo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) NewBatchEx(options store.KVBatchOptions) ([]byte, store.KVBatch, error) {
|
||||||
|
return make([]byte, options.TotalBytes), w.NewBatch(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) ExecuteBatch(batch store.KVBatch) error {
|
||||||
|
|
||||||
|
emulatedBatch, ok := batch.(*store.EmulatedBatch)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("wrong type of batch")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.s.m.Lock()
|
||||||
|
for k, mergeOps := range emulatedBatch.Merger.Merges {
|
||||||
|
kb := []byte(k)
|
||||||
|
var existingVal []byte
|
||||||
|
existingItem := w.s.t.Get(&Item{k: kb})
|
||||||
|
if existingItem != nil {
|
||||||
|
existingVal = w.s.t.Get(&Item{k: kb}).(*Item).v
|
||||||
|
}
|
||||||
|
mergedVal, fullMergeOk := w.s.mo.FullMerge(kb, existingVal, mergeOps)
|
||||||
|
if !fullMergeOk {
|
||||||
|
return fmt.Errorf("merge operator returned failure")
|
||||||
|
}
|
||||||
|
w.s.t = w.s.t.Upsert(&Item{k: kb, v: mergedVal}, rand.Int())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, op := range emulatedBatch.Ops {
|
||||||
|
if op.V != nil {
|
||||||
|
w.s.t = w.s.t.Upsert(&Item{k: op.K, v: op.V}, rand.Int())
|
||||||
|
} else {
|
||||||
|
w.s.t = w.s.t.Delete(&Item{k: op.K})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.s.m.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Close() error {
|
||||||
|
w.s = nil
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// KVStore is an abstraction for working with KV stores. Note that
|
||||||
|
// in order to be used with the bleve.registry, it must also implement
|
||||||
|
// a constructor function of the registry.KVStoreConstructor type.
|
||||||
|
type KVStore interface {
|
||||||
|
|
||||||
|
// Writer returns a KVWriter which can be used to
|
||||||
|
// make changes to the KVStore. If a writer cannot
|
||||||
|
// be obtained a non-nil error is returned.
|
||||||
|
Writer() (KVWriter, error)
|
||||||
|
|
||||||
|
// Reader returns a KVReader which can be used to
|
||||||
|
// read data from the KVStore. If a reader cannot
|
||||||
|
// be obtained a non-nil error is returned.
|
||||||
|
Reader() (KVReader, error)
|
||||||
|
|
||||||
|
// Close closes the KVStore
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// KVReader is an abstraction of an **ISOLATED** reader
|
||||||
|
// In this context isolated is defined to mean that
|
||||||
|
// writes/deletes made after the KVReader is opened
|
||||||
|
// are not observed.
|
||||||
|
// Because there is usually a cost associated with
|
||||||
|
// keeping isolated readers active, users should
|
||||||
|
// close them as soon as they are no longer needed.
|
||||||
|
type KVReader interface {
|
||||||
|
|
||||||
|
// Get returns the value associated with the key
|
||||||
|
// If the key does not exist, nil is returned.
|
||||||
|
// The caller owns the bytes returned.
|
||||||
|
Get(key []byte) ([]byte, error)
|
||||||
|
|
||||||
|
// MultiGet retrieves multiple values in one call.
|
||||||
|
MultiGet(keys [][]byte) ([][]byte, error)
|
||||||
|
|
||||||
|
// PrefixIterator returns a KVIterator that will
|
||||||
|
// visit all K/V pairs with the provided prefix
|
||||||
|
PrefixIterator(prefix []byte) KVIterator
|
||||||
|
|
||||||
|
// RangeIterator returns a KVIterator that will
|
||||||
|
// visit all K/V pairs >= start AND < end
|
||||||
|
RangeIterator(start, end []byte) KVIterator
|
||||||
|
|
||||||
|
// Close closes the iterator
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// KVIterator is an abstraction around key iteration
|
||||||
|
type KVIterator interface {
|
||||||
|
|
||||||
|
// Seek will advance the iterator to the specified key
|
||||||
|
Seek(key []byte)
|
||||||
|
|
||||||
|
// Next will advance the iterator to the next key
|
||||||
|
Next()
|
||||||
|
|
||||||
|
// Key returns the key pointed to by the iterator
|
||||||
|
// The bytes returned are **ONLY** valid until the next call to Seek/Next/Close
|
||||||
|
// Continued use after that requires that they be copied.
|
||||||
|
Key() []byte
|
||||||
|
|
||||||
|
// Value returns the value pointed to by the iterator
|
||||||
|
// The bytes returned are **ONLY** valid until the next call to Seek/Next/Close
|
||||||
|
// Continued use after that requires that they be copied.
|
||||||
|
Value() []byte
|
||||||
|
|
||||||
|
// Valid returns whether or not the iterator is in a valid state
|
||||||
|
Valid() bool
|
||||||
|
|
||||||
|
// Current returns Key(),Value(),Valid() in a single operation
|
||||||
|
Current() ([]byte, []byte, bool)
|
||||||
|
|
||||||
|
// Close closes the iterator
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// KVWriter is an abstraction for mutating the KVStore
|
||||||
|
// KVWriter does **NOT** enforce restrictions of a single writer
|
||||||
|
// if the underlying KVStore allows concurrent writes, the
|
||||||
|
// KVWriter interface should also do so, it is up to the caller
|
||||||
|
// to do this in a way that is safe and makes sense
|
||||||
|
type KVWriter interface {
|
||||||
|
|
||||||
|
// NewBatch returns a KVBatch for performing batch operations on this kvstore
|
||||||
|
NewBatch() KVBatch
|
||||||
|
|
||||||
|
// NewBatchEx returns a KVBatch and an associated byte array
|
||||||
|
// that's pre-sized based on the KVBatchOptions. The caller can
|
||||||
|
// use the returned byte array for keys and values associated with
|
||||||
|
// the batch. Once the batch is either executed or closed, the
|
||||||
|
// associated byte array should no longer be accessed by the
|
||||||
|
// caller.
|
||||||
|
NewBatchEx(KVBatchOptions) ([]byte, KVBatch, error)
|
||||||
|
|
||||||
|
// ExecuteBatch will execute the KVBatch, the provided KVBatch **MUST** have
|
||||||
|
// been created by the same KVStore (though not necessarily the same KVWriter)
|
||||||
|
// Batch execution is atomic, either all the operations or none will be performed
|
||||||
|
ExecuteBatch(batch KVBatch) error
|
||||||
|
|
||||||
|
// Close closes the writer
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// KVBatchOptions provides the KVWriter.NewBatchEx() method with batch
|
||||||
|
// preparation and preallocation information.
|
||||||
|
type KVBatchOptions struct {
|
||||||
|
// TotalBytes is the sum of key and value bytes needed by the
|
||||||
|
// caller for the entire batch. It affects the size of the
|
||||||
|
// returned byte array of KVWrite.NewBatchEx().
|
||||||
|
TotalBytes int
|
||||||
|
|
||||||
|
// NumSets is the number of Set() calls the caller will invoke on
|
||||||
|
// the KVBatch.
|
||||||
|
NumSets int
|
||||||
|
|
||||||
|
// NumDeletes is the number of Delete() calls the caller will invoke
|
||||||
|
// on the KVBatch.
|
||||||
|
NumDeletes int
|
||||||
|
|
||||||
|
// NumMerges is the number of Merge() calls the caller will invoke
|
||||||
|
// on the KVBatch.
|
||||||
|
NumMerges int
|
||||||
|
}
|
||||||
|
|
||||||
|
// KVBatch is an abstraction for making multiple KV mutations at once
|
||||||
|
type KVBatch interface {
|
||||||
|
|
||||||
|
// Set updates the key with the specified value
|
||||||
|
// both key and value []byte may be reused as soon as this call returns
|
||||||
|
Set(key, val []byte)
|
||||||
|
|
||||||
|
// Delete removes the specified key
|
||||||
|
// the key []byte may be reused as soon as this call returns
|
||||||
|
Delete(key []byte)
|
||||||
|
|
||||||
|
// Merge merges old value with the new value at the specified key
|
||||||
|
// as prescribed by the KVStores merge operator
|
||||||
|
// both key and value []byte may be reused as soon as this call returns
|
||||||
|
Merge(key, val []byte)
|
||||||
|
|
||||||
|
// Reset frees resources for this batch and allows reuse
|
||||||
|
Reset()
|
||||||
|
|
||||||
|
// Close frees resources
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// KVStoreStats is an optional interface that KVStores can implement
|
||||||
|
// if they're able to report any useful stats
|
||||||
|
type KVStoreStats interface {
|
||||||
|
// Stats returns a JSON serializable object representing stats for this KVStore
|
||||||
|
Stats() json.Marshaler
|
||||||
|
|
||||||
|
StatsMap() map[string]interface{}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
// At the moment this happens to be the same interface as described by
|
||||||
|
// RocksDB, but this may not always be the case.
|
||||||
|
|
||||||
|
type MergeOperator interface {
|
||||||
|
|
||||||
|
// FullMerge the full sequence of operands on top of the existingValue
|
||||||
|
// if no value currently exists, existingValue is nil
|
||||||
|
// return the merged value, and success/failure
|
||||||
|
FullMerge(key, existingValue []byte, operands [][]byte) ([]byte, bool)
|
||||||
|
|
||||||
|
// Partially merge these two operands.
|
||||||
|
// If partial merge cannot be done, return nil,false, which will defer
|
||||||
|
// all processing until the FullMerge is done.
|
||||||
|
PartialMerge(key, leftOperand, rightOperand []byte) ([]byte, bool)
|
||||||
|
|
||||||
|
// Name returns an identifier for the operator
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmulatedMerge struct {
|
||||||
|
Merges map[string][][]byte
|
||||||
|
mo MergeOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmulatedMerge(mo MergeOperator) *EmulatedMerge {
|
||||||
|
return &EmulatedMerge{
|
||||||
|
Merges: make(map[string][][]byte),
|
||||||
|
mo: mo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *EmulatedMerge) Merge(key, val []byte) {
|
||||||
|
ops, ok := m.Merges[string(key)]
|
||||||
|
if ok && len(ops) > 0 {
|
||||||
|
last := ops[len(ops)-1]
|
||||||
|
mergedVal, partialMergeOk := m.mo.PartialMerge(key, last, val)
|
||||||
|
if partialMergeOk {
|
||||||
|
// replace last entry with the result of the merge
|
||||||
|
ops[len(ops)-1] = mergedVal
|
||||||
|
} else {
|
||||||
|
// could not partial merge, append this to the end
|
||||||
|
ops = append(ops, val)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ops = [][]byte{val}
|
||||||
|
}
|
||||||
|
m.Merges[string(key)] = ops
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright (c) 2016 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
// MultiGet is a helper function to retrieve mutiple keys from a
|
||||||
|
// KVReader, and might be used by KVStore implementations that don't
|
||||||
|
// have a native multi-get facility.
|
||||||
|
func MultiGet(kvreader KVReader, keys [][]byte) ([][]byte, error) {
|
||||||
|
vals := make([][]byte, 0, len(keys))
|
||||||
|
|
||||||
|
for i, key := range keys {
|
||||||
|
val, err := kvreader.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vals[i] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
return vals, nil
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright (c) 2015 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package upsidedown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/document"
|
||||||
|
"github.com/blevesearch/bleve/index"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (udc *UpsideDownCouch) Analyze(d *document.Document) *index.AnalysisResult {
|
||||||
|
rv := &index.AnalysisResult{
|
||||||
|
DocID: d.ID,
|
||||||
|
Rows: make([]index.IndexRow, 0, 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
docIDBytes := []byte(d.ID)
|
||||||
|
|
||||||
|
// track our back index entries
|
||||||
|
backIndexStoredEntries := make([]*BackIndexStoreEntry, 0)
|
||||||
|
|
||||||
|
// information we collate as we merge fields with same name
|
||||||
|
fieldTermFreqs := make(map[uint16]analysis.TokenFrequencies)
|
||||||
|
fieldLengths := make(map[uint16]int)
|
||||||
|
fieldIncludeTermVectors := make(map[uint16]bool)
|
||||||
|
fieldNames := make(map[uint16]string)
|
||||||
|
|
||||||
|
analyzeField := func(field document.Field, storable bool) {
|
||||||
|
fieldIndex, newFieldRow := udc.fieldIndexOrNewRow(field.Name())
|
||||||
|
if newFieldRow != nil {
|
||||||
|
rv.Rows = append(rv.Rows, newFieldRow)
|
||||||
|
}
|
||||||
|
fieldNames[fieldIndex] = field.Name()
|
||||||
|
|
||||||
|
if field.Options().IsIndexed() {
|
||||||
|
fieldLength, tokenFreqs := field.Analyze()
|
||||||
|
existingFreqs := fieldTermFreqs[fieldIndex]
|
||||||
|
if existingFreqs == nil {
|
||||||
|
fieldTermFreqs[fieldIndex] = tokenFreqs
|
||||||
|
} else {
|
||||||
|
existingFreqs.MergeAll(field.Name(), tokenFreqs)
|
||||||
|
fieldTermFreqs[fieldIndex] = existingFreqs
|
||||||
|
}
|
||||||
|
fieldLengths[fieldIndex] += fieldLength
|
||||||
|
fieldIncludeTermVectors[fieldIndex] = field.Options().IncludeTermVectors()
|
||||||
|
}
|
||||||
|
|
||||||
|
if storable && field.Options().IsStored() {
|
||||||
|
rv.Rows, backIndexStoredEntries = udc.storeField(docIDBytes, field, fieldIndex, rv.Rows, backIndexStoredEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk all the fields, record stored fields now
|
||||||
|
// place information about indexed fields into map
|
||||||
|
// this collates information across fields with
|
||||||
|
// same names (arrays)
|
||||||
|
for _, field := range d.Fields {
|
||||||
|
analyzeField(field, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.CompositeFields) > 0 {
|
||||||
|
for fieldIndex, tokenFreqs := range fieldTermFreqs {
|
||||||
|
// see if any of the composite fields need this
|
||||||
|
for _, compositeField := range d.CompositeFields {
|
||||||
|
compositeField.Compose(fieldNames[fieldIndex], fieldLengths[fieldIndex], tokenFreqs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, compositeField := range d.CompositeFields {
|
||||||
|
analyzeField(compositeField, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsCapNeeded := len(rv.Rows) + 1
|
||||||
|
for _, tokenFreqs := range fieldTermFreqs {
|
||||||
|
rowsCapNeeded += len(tokenFreqs)
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.Rows = append(make([]index.IndexRow, 0, rowsCapNeeded), rv.Rows...)
|
||||||
|
|
||||||
|
backIndexTermEntries := make([]*BackIndexTermEntry, 0, rowsCapNeeded)
|
||||||
|
|
||||||
|
// walk through the collated information and process
|
||||||
|
// once for each indexed field (unique name)
|
||||||
|
for fieldIndex, tokenFreqs := range fieldTermFreqs {
|
||||||
|
fieldLength := fieldLengths[fieldIndex]
|
||||||
|
includeTermVectors := fieldIncludeTermVectors[fieldIndex]
|
||||||
|
|
||||||
|
// encode this field
|
||||||
|
rv.Rows, backIndexTermEntries = udc.indexField(docIDBytes, includeTermVectors, fieldIndex, fieldLength, tokenFreqs, rv.Rows, backIndexTermEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the back index row
|
||||||
|
backIndexRow := NewBackIndexRow(docIDBytes, backIndexTermEntries, backIndexStoredEntries)
|
||||||
|
rv.Rows = append(rv.Rows, backIndexRow)
|
||||||
|
|
||||||
|
return rv
|
||||||
|
}
|
8
vendor/github.com/blevesearch/bleve/index/upsidedown/benchmark_all.sh
generated
vendored
Executable file
8
vendor/github.com/blevesearch/bleve/index/upsidedown/benchmark_all.sh
generated
vendored
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
BENCHMARKS=`grep "func Benchmark" *_test.go | sed 's/.*func //' | sed s/\(.*{//`
|
||||||
|
|
||||||
|
for BENCHMARK in $BENCHMARKS
|
||||||
|
do
|
||||||
|
go test -v -run=xxx -bench=^$BENCHMARK$ -benchtime=10s -tags 'forestdb leveldb' | grep -v ok | grep -v PASS
|
||||||
|
done
|
|
@ -0,0 +1,172 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package upsidedown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// the functions in this file are only intended to be used by
|
||||||
|
// the bleve_dump utility and the debug http handlers
|
||||||
|
// if your application relies on them, you're doing something wrong
|
||||||
|
// they may change or be removed at any time
|
||||||
|
|
||||||
|
func dumpPrefix(kvreader store.KVReader, rv chan interface{}, prefix []byte) {
|
||||||
|
start := prefix
|
||||||
|
if start == nil {
|
||||||
|
start = []byte{0}
|
||||||
|
}
|
||||||
|
it := kvreader.PrefixIterator(start)
|
||||||
|
defer func() {
|
||||||
|
cerr := it.Close()
|
||||||
|
if cerr != nil {
|
||||||
|
rv <- cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
key, val, valid := it.Current()
|
||||||
|
for valid {
|
||||||
|
ck := make([]byte, len(key))
|
||||||
|
copy(ck, key)
|
||||||
|
cv := make([]byte, len(val))
|
||||||
|
copy(cv, val)
|
||||||
|
row, err := ParseFromKeyValue(ck, cv)
|
||||||
|
if err != nil {
|
||||||
|
rv <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rv <- row
|
||||||
|
|
||||||
|
it.Next()
|
||||||
|
key, val, valid = it.Current()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpRange(kvreader store.KVReader, rv chan interface{}, start, end []byte) {
|
||||||
|
it := kvreader.RangeIterator(start, end)
|
||||||
|
defer func() {
|
||||||
|
cerr := it.Close()
|
||||||
|
if cerr != nil {
|
||||||
|
rv <- cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
key, val, valid := it.Current()
|
||||||
|
for valid {
|
||||||
|
ck := make([]byte, len(key))
|
||||||
|
copy(ck, key)
|
||||||
|
cv := make([]byte, len(val))
|
||||||
|
copy(cv, val)
|
||||||
|
row, err := ParseFromKeyValue(ck, cv)
|
||||||
|
if err != nil {
|
||||||
|
rv <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rv <- row
|
||||||
|
|
||||||
|
it.Next()
|
||||||
|
key, val, valid = it.Current()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) DumpAll() chan interface{} {
|
||||||
|
rv := make(chan interface{})
|
||||||
|
go func() {
|
||||||
|
defer close(rv)
|
||||||
|
dumpRange(i.kvreader, rv, nil, nil)
|
||||||
|
}()
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) DumpFields() chan interface{} {
|
||||||
|
rv := make(chan interface{})
|
||||||
|
go func() {
|
||||||
|
defer close(rv)
|
||||||
|
dumpPrefix(i.kvreader, rv, []byte{'f'})
|
||||||
|
}()
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyset [][]byte
|
||||||
|
|
||||||
|
func (k keyset) Len() int { return len(k) }
|
||||||
|
func (k keyset) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
|
||||||
|
func (k keyset) Less(i, j int) bool { return bytes.Compare(k[i], k[j]) < 0 }
|
||||||
|
|
||||||
|
// DumpDoc returns all rows in the index related to this doc id
|
||||||
|
func (i *IndexReader) DumpDoc(id string) chan interface{} {
|
||||||
|
idBytes := []byte(id)
|
||||||
|
|
||||||
|
rv := make(chan interface{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(rv)
|
||||||
|
|
||||||
|
back, err := backIndexRowForDoc(i.kvreader, []byte(id))
|
||||||
|
if err != nil {
|
||||||
|
rv <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// no such doc
|
||||||
|
if back == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// build sorted list of term keys
|
||||||
|
keys := make(keyset, 0)
|
||||||
|
for _, entry := range back.termEntries {
|
||||||
|
tfr := NewTermFrequencyRow([]byte(*entry.Term), uint16(*entry.Field), idBytes, 0, 0)
|
||||||
|
key := tfr.Key()
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Sort(keys)
|
||||||
|
|
||||||
|
// first add all the stored rows
|
||||||
|
storedRowPrefix := NewStoredRow(idBytes, 0, []uint64{}, 'x', []byte{}).ScanPrefixForDoc()
|
||||||
|
dumpPrefix(i.kvreader, rv, storedRowPrefix)
|
||||||
|
|
||||||
|
// now walk term keys in order and add them as well
|
||||||
|
if len(keys) > 0 {
|
||||||
|
it := i.kvreader.RangeIterator(keys[0], nil)
|
||||||
|
defer func() {
|
||||||
|
cerr := it.Close()
|
||||||
|
if cerr != nil {
|
||||||
|
rv <- cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
it.Seek(key)
|
||||||
|
rkey, rval, valid := it.Current()
|
||||||
|
if !valid {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
rck := make([]byte, len(rkey))
|
||||||
|
copy(rck, key)
|
||||||
|
rcv := make([]byte, len(rval))
|
||||||
|
copy(rcv, rval)
|
||||||
|
row, err := ParseFromKeyValue(rck, rcv)
|
||||||
|
if err != nil {
|
||||||
|
rv <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rv <- row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return rv
|
||||||
|
}
|
78
vendor/github.com/blevesearch/bleve/index/upsidedown/field_dict.go
generated
vendored
Normal file
78
vendor/github.com/blevesearch/bleve/index/upsidedown/field_dict.go
generated
vendored
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package upsidedown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index"
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpsideDownCouchFieldDict struct {
|
||||||
|
indexReader *IndexReader
|
||||||
|
iterator store.KVIterator
|
||||||
|
dictRow *DictionaryRow
|
||||||
|
dictEntry *index.DictEntry
|
||||||
|
field uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpsideDownCouchFieldDict(indexReader *IndexReader, field uint16, startTerm, endTerm []byte) (*UpsideDownCouchFieldDict, error) {
|
||||||
|
|
||||||
|
startKey := NewDictionaryRow(startTerm, field, 0).Key()
|
||||||
|
if endTerm == nil {
|
||||||
|
endTerm = []byte{ByteSeparator}
|
||||||
|
} else {
|
||||||
|
endTerm = incrementBytes(endTerm)
|
||||||
|
}
|
||||||
|
endKey := NewDictionaryRow(endTerm, field, 0).Key()
|
||||||
|
|
||||||
|
it := indexReader.kvreader.RangeIterator(startKey, endKey)
|
||||||
|
|
||||||
|
return &UpsideDownCouchFieldDict{
|
||||||
|
indexReader: indexReader,
|
||||||
|
iterator: it,
|
||||||
|
dictRow: &DictionaryRow{}, // Pre-alloced, reused row.
|
||||||
|
dictEntry: &index.DictEntry{}, // Pre-alloced, reused entry.
|
||||||
|
field: field,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpsideDownCouchFieldDict) Next() (*index.DictEntry, error) {
|
||||||
|
key, val, valid := r.iterator.Current()
|
||||||
|
if !valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.dictRow.parseDictionaryK(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unexpected error parsing dictionary row key: %v", err)
|
||||||
|
}
|
||||||
|
err = r.dictRow.parseDictionaryV(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unexpected error parsing dictionary row val: %v", err)
|
||||||
|
}
|
||||||
|
r.dictEntry.Term = string(r.dictRow.term)
|
||||||
|
r.dictEntry.Count = r.dictRow.count
|
||||||
|
// advance the iterator to the next term
|
||||||
|
r.iterator.Next()
|
||||||
|
return r.dictEntry, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpsideDownCouchFieldDict) Close() error {
|
||||||
|
return r.iterator.Close()
|
||||||
|
}
|
189
vendor/github.com/blevesearch/bleve/index/upsidedown/index_reader.go
generated
vendored
Normal file
189
vendor/github.com/blevesearch/bleve/index/upsidedown/index_reader.go
generated
vendored
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package upsidedown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/blevesearch/bleve/document"
|
||||||
|
"github.com/blevesearch/bleve/index"
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexReader struct {
|
||||||
|
index *UpsideDownCouch
|
||||||
|
kvreader store.KVReader
|
||||||
|
docCount uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) TermFieldReader(term []byte, fieldName string, includeFreq, includeNorm, includeTermVectors bool) (index.TermFieldReader, error) {
|
||||||
|
fieldIndex, fieldExists := i.index.fieldCache.FieldNamed(fieldName, false)
|
||||||
|
if fieldExists {
|
||||||
|
return newUpsideDownCouchTermFieldReader(i, term, uint16(fieldIndex), includeFreq, includeNorm, includeTermVectors)
|
||||||
|
}
|
||||||
|
return newUpsideDownCouchTermFieldReader(i, []byte{ByteSeparator}, ^uint16(0), includeFreq, includeNorm, includeTermVectors)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) FieldDict(fieldName string) (index.FieldDict, error) {
|
||||||
|
return i.FieldDictRange(fieldName, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) FieldDictRange(fieldName string, startTerm []byte, endTerm []byte) (index.FieldDict, error) {
|
||||||
|
fieldIndex, fieldExists := i.index.fieldCache.FieldNamed(fieldName, false)
|
||||||
|
if fieldExists {
|
||||||
|
return newUpsideDownCouchFieldDict(i, uint16(fieldIndex), startTerm, endTerm)
|
||||||
|
}
|
||||||
|
return newUpsideDownCouchFieldDict(i, ^uint16(0), []byte{ByteSeparator}, []byte{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) FieldDictPrefix(fieldName string, termPrefix []byte) (index.FieldDict, error) {
|
||||||
|
return i.FieldDictRange(fieldName, termPrefix, termPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) DocIDReaderAll() (index.DocIDReader, error) {
|
||||||
|
return newUpsideDownCouchDocIDReader(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) DocIDReaderOnly(ids []string) (index.DocIDReader, error) {
|
||||||
|
return newUpsideDownCouchDocIDReaderOnly(i, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) Document(id string) (doc *document.Document, err error) {
|
||||||
|
// first hit the back index to confirm doc exists
|
||||||
|
var backIndexRow *BackIndexRow
|
||||||
|
backIndexRow, err = backIndexRowForDoc(i.kvreader, []byte(id))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if backIndexRow == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
doc = document.NewDocument(id)
|
||||||
|
storedRow := NewStoredRow([]byte(id), 0, []uint64{}, 'x', nil)
|
||||||
|
storedRowScanPrefix := storedRow.ScanPrefixForDoc()
|
||||||
|
it := i.kvreader.PrefixIterator(storedRowScanPrefix)
|
||||||
|
defer func() {
|
||||||
|
if cerr := it.Close(); err == nil && cerr != nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
key, val, valid := it.Current()
|
||||||
|
for valid {
|
||||||
|
safeVal := make([]byte, len(val))
|
||||||
|
copy(safeVal, val)
|
||||||
|
var row *StoredRow
|
||||||
|
row, err = NewStoredRowKV(key, safeVal)
|
||||||
|
if err != nil {
|
||||||
|
doc = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if row != nil {
|
||||||
|
fieldName := i.index.fieldCache.FieldIndexed(row.field)
|
||||||
|
field := decodeFieldType(row.typ, fieldName, row.arrayPositions, row.value)
|
||||||
|
if field != nil {
|
||||||
|
doc.AddField(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it.Next()
|
||||||
|
key, val, valid = it.Current()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) DocumentFieldTerms(id index.IndexInternalID, fields []string) (index.FieldTerms, error) {
|
||||||
|
back, err := backIndexRowForDoc(i.kvreader, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if back == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
rv := make(index.FieldTerms, len(fields))
|
||||||
|
fieldsMap := make(map[uint16]string, len(fields))
|
||||||
|
for _, f := range fields {
|
||||||
|
id, ok := i.index.fieldCache.FieldNamed(f, false)
|
||||||
|
if ok {
|
||||||
|
fieldsMap[id] = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, entry := range back.termEntries {
|
||||||
|
if field, ok := fieldsMap[uint16(*entry.Field)]; ok {
|
||||||
|
rv[field] = append(rv[field], *entry.Term)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) Fields() (fields []string, err error) {
|
||||||
|
fields = make([]string, 0)
|
||||||
|
it := i.kvreader.PrefixIterator([]byte{'f'})
|
||||||
|
defer func() {
|
||||||
|
if cerr := it.Close(); err == nil && cerr != nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
key, val, valid := it.Current()
|
||||||
|
for valid {
|
||||||
|
var row UpsideDownCouchRow
|
||||||
|
row, err = ParseFromKeyValue(key, val)
|
||||||
|
if err != nil {
|
||||||
|
fields = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if row != nil {
|
||||||
|
fieldRow, ok := row.(*FieldRow)
|
||||||
|
if ok {
|
||||||
|
fields = append(fields, fieldRow.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it.Next()
|
||||||
|
key, val, valid = it.Current()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) GetInternal(key []byte) ([]byte, error) {
|
||||||
|
internalRow := NewInternalRow(key, nil)
|
||||||
|
return i.kvreader.Get(internalRow.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) DocCount() (uint64, error) {
|
||||||
|
return i.docCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) Close() error {
|
||||||
|
return i.kvreader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) ExternalID(id index.IndexInternalID) (string, error) {
|
||||||
|
return string(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexReader) InternalID(id string) (index.IndexInternalID, error) {
|
||||||
|
return index.IndexInternalID(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func incrementBytes(in []byte) []byte {
|
||||||
|
rv := make([]byte, len(in))
|
||||||
|
copy(rv, in)
|
||||||
|
for i := len(rv) - 1; i >= 0; i-- {
|
||||||
|
rv[i] = rv[i] + 1
|
||||||
|
if rv[i] != 0 {
|
||||||
|
// didn't overflow, so stop
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
|
@ -0,0 +1,325 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package upsidedown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index"
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpsideDownCouchTermFieldReader struct {
|
||||||
|
count uint64
|
||||||
|
indexReader *IndexReader
|
||||||
|
iterator store.KVIterator
|
||||||
|
term []byte
|
||||||
|
tfrNext *TermFrequencyRow
|
||||||
|
keyBuf []byte
|
||||||
|
field uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpsideDownCouchTermFieldReader(indexReader *IndexReader, term []byte, field uint16, includeFreq, includeNorm, includeTermVectors bool) (*UpsideDownCouchTermFieldReader, error) {
|
||||||
|
dictionaryRow := NewDictionaryRow(term, field, 0)
|
||||||
|
val, err := indexReader.kvreader.Get(dictionaryRow.Key())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if val == nil {
|
||||||
|
atomic.AddUint64(&indexReader.index.stats.termSearchersStarted, uint64(1))
|
||||||
|
return &UpsideDownCouchTermFieldReader{
|
||||||
|
count: 0,
|
||||||
|
term: term,
|
||||||
|
tfrNext: &TermFrequencyRow{},
|
||||||
|
field: field,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dictionaryRow.parseDictionaryV(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tfr := NewTermFrequencyRow(term, field, []byte{}, 0, 0)
|
||||||
|
it := indexReader.kvreader.PrefixIterator(tfr.Key())
|
||||||
|
|
||||||
|
atomic.AddUint64(&indexReader.index.stats.termSearchersStarted, uint64(1))
|
||||||
|
return &UpsideDownCouchTermFieldReader{
|
||||||
|
indexReader: indexReader,
|
||||||
|
iterator: it,
|
||||||
|
count: dictionaryRow.count,
|
||||||
|
term: term,
|
||||||
|
field: field,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpsideDownCouchTermFieldReader) Count() uint64 {
|
||||||
|
return r.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpsideDownCouchTermFieldReader) Next(preAlloced *index.TermFieldDoc) (*index.TermFieldDoc, error) {
|
||||||
|
if r.iterator != nil {
|
||||||
|
// We treat tfrNext also like an initialization flag, which
|
||||||
|
// tells us whether we need to invoke the underlying
|
||||||
|
// iterator.Next(). The first time, don't call iterator.Next().
|
||||||
|
if r.tfrNext != nil {
|
||||||
|
r.iterator.Next()
|
||||||
|
} else {
|
||||||
|
r.tfrNext = &TermFrequencyRow{}
|
||||||
|
}
|
||||||
|
key, val, valid := r.iterator.Current()
|
||||||
|
if valid {
|
||||||
|
tfr := r.tfrNext
|
||||||
|
err := tfr.parseKDoc(key, r.term)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tfr.parseV(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv := preAlloced
|
||||||
|
if rv == nil {
|
||||||
|
rv = &index.TermFieldDoc{}
|
||||||
|
}
|
||||||
|
rv.ID = append(rv.ID, tfr.doc...)
|
||||||
|
rv.Freq = tfr.freq
|
||||||
|
rv.Norm = float64(tfr.norm)
|
||||||
|
if tfr.vectors != nil {
|
||||||
|
rv.Vectors = r.indexReader.index.termFieldVectorsFromTermVectors(tfr.vectors)
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpsideDownCouchTermFieldReader) Advance(docID index.IndexInternalID, preAlloced *index.TermFieldDoc) (rv *index.TermFieldDoc, err error) {
|
||||||
|
if r.iterator != nil {
|
||||||
|
if r.tfrNext == nil {
|
||||||
|
r.tfrNext = &TermFrequencyRow{}
|
||||||
|
}
|
||||||
|
tfr := InitTermFrequencyRow(r.tfrNext, r.term, r.field, docID, 0, 0)
|
||||||
|
r.keyBuf, err = tfr.KeyAppendTo(r.keyBuf[:0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.iterator.Seek(r.keyBuf)
|
||||||
|
key, val, valid := r.iterator.Current()
|
||||||
|
if valid {
|
||||||
|
err := tfr.parseKDoc(key, r.term)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tfr.parseV(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv = preAlloced
|
||||||
|
if rv == nil {
|
||||||
|
rv = &index.TermFieldDoc{}
|
||||||
|
}
|
||||||
|
rv.ID = append(rv.ID, tfr.doc...)
|
||||||
|
rv.Freq = tfr.freq
|
||||||
|
rv.Norm = float64(tfr.norm)
|
||||||
|
if tfr.vectors != nil {
|
||||||
|
rv.Vectors = r.indexReader.index.termFieldVectorsFromTermVectors(tfr.vectors)
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpsideDownCouchTermFieldReader) Close() error {
|
||||||
|
if r.indexReader != nil {
|
||||||
|
atomic.AddUint64(&r.indexReader.index.stats.termSearchersFinished, uint64(1))
|
||||||
|
}
|
||||||
|
if r.iterator != nil {
|
||||||
|
return r.iterator.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpsideDownCouchDocIDReader struct {
|
||||||
|
indexReader *IndexReader
|
||||||
|
iterator store.KVIterator
|
||||||
|
only []string
|
||||||
|
onlyPos int
|
||||||
|
onlyMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpsideDownCouchDocIDReader(indexReader *IndexReader) (*UpsideDownCouchDocIDReader, error) {
|
||||||
|
|
||||||
|
startBytes := []byte{0x0}
|
||||||
|
endBytes := []byte{0xff}
|
||||||
|
|
||||||
|
bisr := NewBackIndexRow(startBytes, nil, nil)
|
||||||
|
bier := NewBackIndexRow(endBytes, nil, nil)
|
||||||
|
it := indexReader.kvreader.RangeIterator(bisr.Key(), bier.Key())
|
||||||
|
|
||||||
|
return &UpsideDownCouchDocIDReader{
|
||||||
|
indexReader: indexReader,
|
||||||
|
iterator: it,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpsideDownCouchDocIDReaderOnly(indexReader *IndexReader, ids []string) (*UpsideDownCouchDocIDReader, error) {
|
||||||
|
// ensure ids are sorted
|
||||||
|
sort.Strings(ids)
|
||||||
|
startBytes := []byte{0x0}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
startBytes = []byte(ids[0])
|
||||||
|
}
|
||||||
|
endBytes := []byte{0xff}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
endBytes = incrementBytes([]byte(ids[len(ids)-1]))
|
||||||
|
}
|
||||||
|
bisr := NewBackIndexRow(startBytes, nil, nil)
|
||||||
|
bier := NewBackIndexRow(endBytes, nil, nil)
|
||||||
|
it := indexReader.kvreader.RangeIterator(bisr.Key(), bier.Key())
|
||||||
|
|
||||||
|
return &UpsideDownCouchDocIDReader{
|
||||||
|
indexReader: indexReader,
|
||||||
|
iterator: it,
|
||||||
|
only: ids,
|
||||||
|
onlyMode: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpsideDownCouchDocIDReader) Next() (index.IndexInternalID, error) {
|
||||||
|
key, val, valid := r.iterator.Current()
|
||||||
|
|
||||||
|
if r.onlyMode {
|
||||||
|
var rv index.IndexInternalID
|
||||||
|
for valid && r.onlyPos < len(r.only) {
|
||||||
|
br, err := NewBackIndexRowKV(key, val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(br.doc, []byte(r.only[r.onlyPos])) {
|
||||||
|
ok := r.nextOnly()
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key())
|
||||||
|
key, val, valid = r.iterator.Current()
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
rv = append([]byte(nil), br.doc...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if valid && r.onlyPos < len(r.only) {
|
||||||
|
ok := r.nextOnly()
|
||||||
|
if ok {
|
||||||
|
r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key())
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if valid {
|
||||||
|
br, err := NewBackIndexRowKV(key, val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv := append([]byte(nil), br.doc...)
|
||||||
|
r.iterator.Next()
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpsideDownCouchDocIDReader) Advance(docID index.IndexInternalID) (index.IndexInternalID, error) {
|
||||||
|
|
||||||
|
if r.onlyMode {
|
||||||
|
r.onlyPos = sort.SearchStrings(r.only, string(docID))
|
||||||
|
if r.onlyPos >= len(r.only) {
|
||||||
|
// advanced to key after our last only key
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key())
|
||||||
|
key, val, valid := r.iterator.Current()
|
||||||
|
|
||||||
|
var rv index.IndexInternalID
|
||||||
|
for valid && r.onlyPos < len(r.only) {
|
||||||
|
br, err := NewBackIndexRowKV(key, val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(br.doc, []byte(r.only[r.onlyPos])) {
|
||||||
|
// the only key we seek'd to didn't exist
|
||||||
|
// now look for the closest key that did exist in only
|
||||||
|
r.onlyPos = sort.SearchStrings(r.only, string(br.doc))
|
||||||
|
if r.onlyPos >= len(r.only) {
|
||||||
|
// advanced to key after our last only key
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
// now seek to this new only key
|
||||||
|
r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key())
|
||||||
|
key, val, valid = r.iterator.Current()
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
rv = append([]byte(nil), br.doc...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if valid && r.onlyPos < len(r.only) {
|
||||||
|
ok := r.nextOnly()
|
||||||
|
if ok {
|
||||||
|
r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key())
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bir := NewBackIndexRow(docID, nil, nil)
|
||||||
|
r.iterator.Seek(bir.Key())
|
||||||
|
key, val, valid := r.iterator.Current()
|
||||||
|
if valid {
|
||||||
|
br, err := NewBackIndexRowKV(key, val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv := append([]byte(nil), br.doc...)
|
||||||
|
r.iterator.Next()
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpsideDownCouchDocIDReader) Close() error {
|
||||||
|
return r.iterator.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// move the r.only pos forward one, skipping duplicates
|
||||||
|
// return true if there is more data, or false if we got to the end of the list
|
||||||
|
func (r *UpsideDownCouchDocIDReader) nextOnly() bool {
|
||||||
|
|
||||||
|
// advance 1 position, until we see a different key
|
||||||
|
// it's already sorted, so this skips duplicates
|
||||||
|
start := r.onlyPos
|
||||||
|
r.onlyPos++
|
||||||
|
for r.onlyPos < len(r.only) && r.only[r.onlyPos] == r.only[start] {
|
||||||
|
start = r.onlyPos
|
||||||
|
r.onlyPos++
|
||||||
|
}
|
||||||
|
// inidicate if we got to the end of the list
|
||||||
|
return r.onlyPos < len(r.only)
|
||||||
|
}
|
|
@ -0,0 +1,853 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package upsidedown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ByteSeparator byte = 0xff
|
||||||
|
|
||||||
|
type UpsideDownCouchRowStream chan UpsideDownCouchRow
|
||||||
|
|
||||||
|
type UpsideDownCouchRow interface {
|
||||||
|
KeySize() int
|
||||||
|
KeyTo([]byte) (int, error)
|
||||||
|
Key() []byte
|
||||||
|
Value() []byte
|
||||||
|
ValueSize() int
|
||||||
|
ValueTo([]byte) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFromKeyValue(key, value []byte) (UpsideDownCouchRow, error) {
|
||||||
|
if len(key) > 0 {
|
||||||
|
switch key[0] {
|
||||||
|
case 'v':
|
||||||
|
return NewVersionRowKV(key, value)
|
||||||
|
case 'f':
|
||||||
|
return NewFieldRowKV(key, value)
|
||||||
|
case 'd':
|
||||||
|
return NewDictionaryRowKV(key, value)
|
||||||
|
case 't':
|
||||||
|
return NewTermFrequencyRowKV(key, value)
|
||||||
|
case 'b':
|
||||||
|
return NewBackIndexRowKV(key, value)
|
||||||
|
case 's':
|
||||||
|
return NewStoredRowKV(key, value)
|
||||||
|
case 'i':
|
||||||
|
return NewInternalRowKV(key, value)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Unknown field type '%s'", string(key[0]))
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Invalid empty key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// VERSION
|
||||||
|
|
||||||
|
type VersionRow struct {
|
||||||
|
version uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VersionRow) Key() []byte {
|
||||||
|
return []byte{'v'}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VersionRow) KeySize() int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VersionRow) KeyTo(buf []byte) (int, error) {
|
||||||
|
buf[0] = 'v'
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VersionRow) Value() []byte {
|
||||||
|
return []byte{byte(v.version)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VersionRow) ValueSize() int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VersionRow) ValueTo(buf []byte) (int, error) {
|
||||||
|
buf[0] = v.version
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VersionRow) String() string {
|
||||||
|
return fmt.Sprintf("Version: %d", v.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVersionRow(version uint8) *VersionRow {
|
||||||
|
return &VersionRow{
|
||||||
|
version: version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVersionRowKV(key, value []byte) (*VersionRow, error) {
|
||||||
|
rv := VersionRow{}
|
||||||
|
buf := bytes.NewBuffer(value)
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &rv.version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// INTERNAL STORAGE
|
||||||
|
|
||||||
|
type InternalRow struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InternalRow) Key() []byte {
|
||||||
|
buf := make([]byte, i.KeySize())
|
||||||
|
size, _ := i.KeyTo(buf)
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InternalRow) KeySize() int {
|
||||||
|
return len(i.key) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InternalRow) KeyTo(buf []byte) (int, error) {
|
||||||
|
buf[0] = 'i'
|
||||||
|
actual := copy(buf[1:], i.key)
|
||||||
|
return 1 + actual, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InternalRow) Value() []byte {
|
||||||
|
return i.val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InternalRow) ValueSize() int {
|
||||||
|
return len(i.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InternalRow) ValueTo(buf []byte) (int, error) {
|
||||||
|
actual := copy(buf, i.val)
|
||||||
|
return actual, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InternalRow) String() string {
|
||||||
|
return fmt.Sprintf("InternalStore - Key: %s (% x) Val: %s (% x)", i.key, i.key, i.val, i.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInternalRow(key, val []byte) *InternalRow {
|
||||||
|
return &InternalRow{
|
||||||
|
key: key,
|
||||||
|
val: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInternalRowKV(key, value []byte) (*InternalRow, error) {
|
||||||
|
rv := InternalRow{}
|
||||||
|
rv.key = key[1:]
|
||||||
|
rv.val = value
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIELD definition
|
||||||
|
|
||||||
|
type FieldRow struct {
|
||||||
|
index uint16
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldRow) Key() []byte {
|
||||||
|
buf := make([]byte, f.KeySize())
|
||||||
|
size, _ := f.KeyTo(buf)
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldRow) KeySize() int {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldRow) KeyTo(buf []byte) (int, error) {
|
||||||
|
buf[0] = 'f'
|
||||||
|
binary.LittleEndian.PutUint16(buf[1:3], f.index)
|
||||||
|
return 3, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldRow) Value() []byte {
|
||||||
|
return append([]byte(f.name), ByteSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldRow) ValueSize() int {
|
||||||
|
return len(f.name) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldRow) ValueTo(buf []byte) (int, error) {
|
||||||
|
size := copy(buf, f.name)
|
||||||
|
buf[size] = ByteSeparator
|
||||||
|
return size + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldRow) String() string {
|
||||||
|
return fmt.Sprintf("Field: %d Name: %s", f.index, f.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFieldRow(index uint16, name string) *FieldRow {
|
||||||
|
return &FieldRow{
|
||||||
|
index: index,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFieldRowKV(key, value []byte) (*FieldRow, error) {
|
||||||
|
rv := FieldRow{}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(key)
|
||||||
|
_, err := buf.ReadByte() // type
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Read(buf, binary.LittleEndian, &rv.index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = bytes.NewBuffer(value)
|
||||||
|
rv.name, err = buf.ReadString(ByteSeparator)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv.name = rv.name[:len(rv.name)-1] // trim off separator byte
|
||||||
|
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DICTIONARY
|
||||||
|
|
||||||
|
const DictionaryRowMaxValueSize = binary.MaxVarintLen64
|
||||||
|
|
||||||
|
type DictionaryRow struct {
|
||||||
|
term []byte
|
||||||
|
count uint64
|
||||||
|
field uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DictionaryRow) Key() []byte {
|
||||||
|
buf := make([]byte, dr.KeySize())
|
||||||
|
size, _ := dr.KeyTo(buf)
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DictionaryRow) KeySize() int {
|
||||||
|
return len(dr.term) + 3
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DictionaryRow) KeyTo(buf []byte) (int, error) {
|
||||||
|
buf[0] = 'd'
|
||||||
|
binary.LittleEndian.PutUint16(buf[1:3], dr.field)
|
||||||
|
size := copy(buf[3:], dr.term)
|
||||||
|
return size + 3, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DictionaryRow) Value() []byte {
|
||||||
|
buf := make([]byte, dr.ValueSize())
|
||||||
|
size, _ := dr.ValueTo(buf)
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DictionaryRow) ValueSize() int {
|
||||||
|
return DictionaryRowMaxValueSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DictionaryRow) ValueTo(buf []byte) (int, error) {
|
||||||
|
used := binary.PutUvarint(buf, dr.count)
|
||||||
|
return used, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DictionaryRow) String() string {
|
||||||
|
return fmt.Sprintf("Dictionary Term: `%s` Field: %d Count: %d ", string(dr.term), dr.field, dr.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDictionaryRow(term []byte, field uint16, count uint64) *DictionaryRow {
|
||||||
|
return &DictionaryRow{
|
||||||
|
term: term,
|
||||||
|
field: field,
|
||||||
|
count: count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDictionaryRowKV(key, value []byte) (*DictionaryRow, error) {
|
||||||
|
rv, err := NewDictionaryRowK(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rv.parseDictionaryV(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDictionaryRowK(key []byte) (*DictionaryRow, error) {
|
||||||
|
rv := &DictionaryRow{}
|
||||||
|
err := rv.parseDictionaryK(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DictionaryRow) parseDictionaryK(key []byte) error {
|
||||||
|
dr.field = binary.LittleEndian.Uint16(key[1:3])
|
||||||
|
if dr.term != nil {
|
||||||
|
dr.term = dr.term[:0]
|
||||||
|
}
|
||||||
|
dr.term = append(dr.term, key[3:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DictionaryRow) parseDictionaryV(value []byte) error {
|
||||||
|
count, nread := binary.Uvarint(value)
|
||||||
|
if nread <= 0 {
|
||||||
|
return fmt.Errorf("DictionaryRow parse Uvarint error, nread: %d", nread)
|
||||||
|
}
|
||||||
|
dr.count = count
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TERM FIELD FREQUENCY
|
||||||
|
|
||||||
|
type TermVector struct {
|
||||||
|
field uint16
|
||||||
|
arrayPositions []uint64
|
||||||
|
pos uint64
|
||||||
|
start uint64
|
||||||
|
end uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tv *TermVector) String() string {
|
||||||
|
return fmt.Sprintf("Field: %d Pos: %d Start: %d End %d ArrayPositions: %#v", tv.field, tv.pos, tv.start, tv.end, tv.arrayPositions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TermFrequencyRow struct {
|
||||||
|
term []byte
|
||||||
|
doc []byte
|
||||||
|
freq uint64
|
||||||
|
vectors []*TermVector
|
||||||
|
norm float32
|
||||||
|
field uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) Term() []byte {
|
||||||
|
return tfr.term
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) Freq() uint64 {
|
||||||
|
return tfr.freq
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) ScanPrefixForField() []byte {
|
||||||
|
buf := make([]byte, 3)
|
||||||
|
buf[0] = 't'
|
||||||
|
binary.LittleEndian.PutUint16(buf[1:3], tfr.field)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) ScanPrefixForFieldTermPrefix() []byte {
|
||||||
|
buf := make([]byte, 3+len(tfr.term))
|
||||||
|
buf[0] = 't'
|
||||||
|
binary.LittleEndian.PutUint16(buf[1:3], tfr.field)
|
||||||
|
copy(buf[3:], tfr.term)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) ScanPrefixForFieldTerm() []byte {
|
||||||
|
buf := make([]byte, 3+len(tfr.term)+1)
|
||||||
|
buf[0] = 't'
|
||||||
|
binary.LittleEndian.PutUint16(buf[1:3], tfr.field)
|
||||||
|
termLen := copy(buf[3:], tfr.term)
|
||||||
|
buf[3+termLen] = ByteSeparator
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) Key() []byte {
|
||||||
|
buf := make([]byte, tfr.KeySize())
|
||||||
|
size, _ := tfr.KeyTo(buf)
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) KeySize() int {
|
||||||
|
return 3 + len(tfr.term) + 1 + len(tfr.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) KeyTo(buf []byte) (int, error) {
|
||||||
|
buf[0] = 't'
|
||||||
|
binary.LittleEndian.PutUint16(buf[1:3], tfr.field)
|
||||||
|
termLen := copy(buf[3:], tfr.term)
|
||||||
|
buf[3+termLen] = ByteSeparator
|
||||||
|
docLen := copy(buf[3+termLen+1:], tfr.doc)
|
||||||
|
return 3 + termLen + 1 + docLen, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) KeyAppendTo(buf []byte) ([]byte, error) {
|
||||||
|
keySize := tfr.KeySize()
|
||||||
|
if cap(buf) < keySize {
|
||||||
|
buf = make([]byte, keySize)
|
||||||
|
}
|
||||||
|
actualSize, err := tfr.KeyTo(buf[0:keySize])
|
||||||
|
return buf[0:actualSize], err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) DictionaryRowKey() []byte {
|
||||||
|
dr := NewDictionaryRow(tfr.term, tfr.field, 0)
|
||||||
|
return dr.Key()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) DictionaryRowKeySize() int {
|
||||||
|
dr := NewDictionaryRow(tfr.term, tfr.field, 0)
|
||||||
|
return dr.KeySize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) DictionaryRowKeyTo(buf []byte) (int, error) {
|
||||||
|
dr := NewDictionaryRow(tfr.term, tfr.field, 0)
|
||||||
|
return dr.KeyTo(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) Value() []byte {
|
||||||
|
buf := make([]byte, tfr.ValueSize())
|
||||||
|
size, _ := tfr.ValueTo(buf)
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) ValueSize() int {
|
||||||
|
bufLen := binary.MaxVarintLen64 + binary.MaxVarintLen64
|
||||||
|
for _, vector := range tfr.vectors {
|
||||||
|
bufLen += (binary.MaxVarintLen64 * 4) + (1+len(vector.arrayPositions))*binary.MaxVarintLen64
|
||||||
|
}
|
||||||
|
return bufLen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) ValueTo(buf []byte) (int, error) {
|
||||||
|
used := binary.PutUvarint(buf[:binary.MaxVarintLen64], tfr.freq)
|
||||||
|
|
||||||
|
normuint32 := math.Float32bits(tfr.norm)
|
||||||
|
newbuf := buf[used : used+binary.MaxVarintLen64]
|
||||||
|
used += binary.PutUvarint(newbuf, uint64(normuint32))
|
||||||
|
|
||||||
|
for _, vector := range tfr.vectors {
|
||||||
|
used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], uint64(vector.field))
|
||||||
|
used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], vector.pos)
|
||||||
|
used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], vector.start)
|
||||||
|
used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], vector.end)
|
||||||
|
used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], uint64(len(vector.arrayPositions)))
|
||||||
|
for _, arrayPosition := range vector.arrayPositions {
|
||||||
|
used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], arrayPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return used, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) String() string {
|
||||||
|
return fmt.Sprintf("Term: `%s` Field: %d DocId: `%s` Frequency: %d Norm: %f Vectors: %v", string(tfr.term), tfr.field, string(tfr.doc), tfr.freq, tfr.norm, tfr.vectors)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitTermFrequencyRow(tfr *TermFrequencyRow, term []byte, field uint16, docID []byte, freq uint64, norm float32) *TermFrequencyRow {
|
||||||
|
tfr.term = term
|
||||||
|
tfr.field = field
|
||||||
|
tfr.doc = docID
|
||||||
|
tfr.freq = freq
|
||||||
|
tfr.norm = norm
|
||||||
|
return tfr
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTermFrequencyRow(term []byte, field uint16, docID []byte, freq uint64, norm float32) *TermFrequencyRow {
|
||||||
|
return &TermFrequencyRow{
|
||||||
|
term: term,
|
||||||
|
field: field,
|
||||||
|
doc: docID,
|
||||||
|
freq: freq,
|
||||||
|
norm: norm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTermFrequencyRowWithTermVectors(term []byte, field uint16, docID []byte, freq uint64, norm float32, vectors []*TermVector) *TermFrequencyRow {
|
||||||
|
return &TermFrequencyRow{
|
||||||
|
term: term,
|
||||||
|
field: field,
|
||||||
|
doc: docID,
|
||||||
|
freq: freq,
|
||||||
|
norm: norm,
|
||||||
|
vectors: vectors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTermFrequencyRowK(key []byte) (*TermFrequencyRow, error) {
|
||||||
|
rv := &TermFrequencyRow{}
|
||||||
|
err := rv.parseK(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) parseK(key []byte) error {
|
||||||
|
keyLen := len(key)
|
||||||
|
if keyLen < 3 {
|
||||||
|
return fmt.Errorf("invalid term frequency key, no valid field")
|
||||||
|
}
|
||||||
|
tfr.field = binary.LittleEndian.Uint16(key[1:3])
|
||||||
|
|
||||||
|
termEndPos := bytes.IndexByte(key[3:], ByteSeparator)
|
||||||
|
if termEndPos < 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency key, no byte separator terminating term")
|
||||||
|
}
|
||||||
|
tfr.term = key[3 : 3+termEndPos]
|
||||||
|
|
||||||
|
docLen := keyLen - (3 + termEndPos + 1)
|
||||||
|
if docLen < 1 {
|
||||||
|
return fmt.Errorf("invalid term frequency key, empty docid")
|
||||||
|
}
|
||||||
|
tfr.doc = key[3+termEndPos+1:]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) parseKDoc(key []byte, term []byte) error {
|
||||||
|
tfr.doc = key[3+len(term)+1:]
|
||||||
|
if len(tfr.doc) <= 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency key, empty docid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfr *TermFrequencyRow) parseV(value []byte) error {
|
||||||
|
var bytesRead int
|
||||||
|
tfr.freq, bytesRead = binary.Uvarint(value)
|
||||||
|
if bytesRead <= 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency value, invalid frequency")
|
||||||
|
}
|
||||||
|
currOffset := bytesRead
|
||||||
|
|
||||||
|
var norm uint64
|
||||||
|
norm, bytesRead = binary.Uvarint(value[currOffset:])
|
||||||
|
if bytesRead <= 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency value, no norm")
|
||||||
|
}
|
||||||
|
currOffset += bytesRead
|
||||||
|
|
||||||
|
tfr.norm = math.Float32frombits(uint32(norm))
|
||||||
|
|
||||||
|
tfr.vectors = nil
|
||||||
|
var field uint64
|
||||||
|
field, bytesRead = binary.Uvarint(value[currOffset:])
|
||||||
|
for bytesRead > 0 {
|
||||||
|
currOffset += bytesRead
|
||||||
|
tv := TermVector{}
|
||||||
|
tv.field = uint16(field)
|
||||||
|
// at this point we expect at least one term vector
|
||||||
|
if tfr.vectors == nil {
|
||||||
|
tfr.vectors = make([]*TermVector, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
tv.pos, bytesRead = binary.Uvarint(value[currOffset:])
|
||||||
|
if bytesRead <= 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency value, vector contains no position")
|
||||||
|
}
|
||||||
|
currOffset += bytesRead
|
||||||
|
|
||||||
|
tv.start, bytesRead = binary.Uvarint(value[currOffset:])
|
||||||
|
if bytesRead <= 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency value, vector contains no start")
|
||||||
|
}
|
||||||
|
currOffset += bytesRead
|
||||||
|
|
||||||
|
tv.end, bytesRead = binary.Uvarint(value[currOffset:])
|
||||||
|
if bytesRead <= 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency value, vector contains no end")
|
||||||
|
}
|
||||||
|
currOffset += bytesRead
|
||||||
|
|
||||||
|
var arrayPositionsLen uint64 = 0
|
||||||
|
arrayPositionsLen, bytesRead = binary.Uvarint(value[currOffset:])
|
||||||
|
if bytesRead <= 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency value, vector contains no arrayPositionLen")
|
||||||
|
}
|
||||||
|
currOffset += bytesRead
|
||||||
|
|
||||||
|
if arrayPositionsLen > 0 {
|
||||||
|
tv.arrayPositions = make([]uint64, arrayPositionsLen)
|
||||||
|
for i := 0; uint64(i) < arrayPositionsLen; i++ {
|
||||||
|
tv.arrayPositions[i], bytesRead = binary.Uvarint(value[currOffset:])
|
||||||
|
if bytesRead <= 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency value, vector contains no arrayPosition of index %d", i)
|
||||||
|
}
|
||||||
|
currOffset += bytesRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tfr.vectors = append(tfr.vectors, &tv)
|
||||||
|
// try to read next record (may not exist)
|
||||||
|
field, bytesRead = binary.Uvarint(value[currOffset:])
|
||||||
|
}
|
||||||
|
if len(value[currOffset:]) > 0 && bytesRead <= 0 {
|
||||||
|
return fmt.Errorf("invalid term frequency value, vector field invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTermFrequencyRowKV(key, value []byte) (*TermFrequencyRow, error) {
|
||||||
|
rv, err := NewTermFrequencyRowK(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rv.parseV(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackIndexRow struct {
|
||||||
|
doc []byte
|
||||||
|
termEntries []*BackIndexTermEntry
|
||||||
|
storedEntries []*BackIndexStoreEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BackIndexRow) AllTermKeys() [][]byte {
|
||||||
|
if br == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rv := make([][]byte, len(br.termEntries))
|
||||||
|
for i, termEntry := range br.termEntries {
|
||||||
|
termRow := NewTermFrequencyRow([]byte(termEntry.GetTerm()), uint16(termEntry.GetField()), br.doc, 0, 0)
|
||||||
|
rv[i] = termRow.Key()
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BackIndexRow) AllStoredKeys() [][]byte {
|
||||||
|
if br == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rv := make([][]byte, len(br.storedEntries))
|
||||||
|
for i, storedEntry := range br.storedEntries {
|
||||||
|
storedRow := NewStoredRow(br.doc, uint16(storedEntry.GetField()), storedEntry.GetArrayPositions(), 'x', []byte{})
|
||||||
|
rv[i] = storedRow.Key()
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BackIndexRow) Key() []byte {
|
||||||
|
buf := make([]byte, br.KeySize())
|
||||||
|
size, _ := br.KeyTo(buf)
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BackIndexRow) KeySize() int {
|
||||||
|
return len(br.doc) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BackIndexRow) KeyTo(buf []byte) (int, error) {
|
||||||
|
buf[0] = 'b'
|
||||||
|
used := copy(buf[1:], br.doc)
|
||||||
|
return used + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BackIndexRow) Value() []byte {
|
||||||
|
buf := make([]byte, br.ValueSize())
|
||||||
|
size, _ := br.ValueTo(buf)
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BackIndexRow) ValueSize() int {
|
||||||
|
birv := &BackIndexRowValue{
|
||||||
|
TermEntries: br.termEntries,
|
||||||
|
StoredEntries: br.storedEntries,
|
||||||
|
}
|
||||||
|
return birv.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BackIndexRow) ValueTo(buf []byte) (int, error) {
|
||||||
|
birv := &BackIndexRowValue{
|
||||||
|
TermEntries: br.termEntries,
|
||||||
|
StoredEntries: br.storedEntries,
|
||||||
|
}
|
||||||
|
return birv.MarshalTo(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BackIndexRow) String() string {
|
||||||
|
return fmt.Sprintf("Backindex DocId: `%s` Term Entries: %v, Stored Entries: %v", string(br.doc), br.termEntries, br.storedEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBackIndexRow(docID []byte, entries []*BackIndexTermEntry, storedFields []*BackIndexStoreEntry) *BackIndexRow {
|
||||||
|
return &BackIndexRow{
|
||||||
|
doc: docID,
|
||||||
|
termEntries: entries,
|
||||||
|
storedEntries: storedFields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBackIndexRowKV(key, value []byte) (*BackIndexRow, error) {
|
||||||
|
rv := BackIndexRow{}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(key)
|
||||||
|
_, err := buf.ReadByte() // type
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.doc, err = buf.ReadBytes(ByteSeparator)
|
||||||
|
if err == io.EOF && len(rv.doc) < 1 {
|
||||||
|
err = fmt.Errorf("invalid doc length 0 - % x", key)
|
||||||
|
}
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
} else if err == nil {
|
||||||
|
rv.doc = rv.doc[:len(rv.doc)-1] // trim off separator byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var birv BackIndexRowValue
|
||||||
|
err = proto.Unmarshal(value, &birv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv.termEntries = birv.TermEntries
|
||||||
|
rv.storedEntries = birv.StoredEntries
|
||||||
|
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// STORED
|
||||||
|
|
||||||
|
type StoredRow struct {
|
||||||
|
doc []byte
|
||||||
|
field uint16
|
||||||
|
arrayPositions []uint64
|
||||||
|
typ byte
|
||||||
|
value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoredRow) Key() []byte {
|
||||||
|
buf := make([]byte, s.KeySize())
|
||||||
|
size, _ := s.KeyTo(buf)
|
||||||
|
return buf[0:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoredRow) KeySize() int {
|
||||||
|
return 1 + len(s.doc) + 1 + 2 + (binary.MaxVarintLen64 * len(s.arrayPositions))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoredRow) KeyTo(buf []byte) (int, error) {
|
||||||
|
docLen := len(s.doc)
|
||||||
|
buf[0] = 's'
|
||||||
|
copy(buf[1:], s.doc)
|
||||||
|
buf[1+docLen] = ByteSeparator
|
||||||
|
binary.LittleEndian.PutUint16(buf[1+docLen+1:], s.field)
|
||||||
|
bytesUsed := 1 + docLen + 1 + 2
|
||||||
|
for _, arrayPosition := range s.arrayPositions {
|
||||||
|
varbytes := binary.PutUvarint(buf[bytesUsed:], arrayPosition)
|
||||||
|
bytesUsed += varbytes
|
||||||
|
}
|
||||||
|
return bytesUsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoredRow) Value() []byte {
|
||||||
|
buf := make([]byte, s.ValueSize())
|
||||||
|
size, _ := s.ValueTo(buf)
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoredRow) ValueSize() int {
|
||||||
|
return len(s.value) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoredRow) ValueTo(buf []byte) (int, error) {
|
||||||
|
buf[0] = s.typ
|
||||||
|
used := copy(buf[1:], s.value)
|
||||||
|
return used + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoredRow) String() string {
|
||||||
|
return fmt.Sprintf("Document: %s Field %d, Array Positions: %v, Type: %s Value: %s", s.doc, s.field, s.arrayPositions, string(s.typ), s.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoredRow) ScanPrefixForDoc() []byte {
|
||||||
|
docLen := len(s.doc)
|
||||||
|
buf := make([]byte, 1+docLen+1)
|
||||||
|
buf[0] = 's'
|
||||||
|
copy(buf[1:], s.doc)
|
||||||
|
buf[1+docLen] = ByteSeparator
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStoredRow(docID []byte, field uint16, arrayPositions []uint64, typ byte, value []byte) *StoredRow {
|
||||||
|
return &StoredRow{
|
||||||
|
doc: docID,
|
||||||
|
field: field,
|
||||||
|
arrayPositions: arrayPositions,
|
||||||
|
typ: typ,
|
||||||
|
value: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStoredRowK(key []byte) (*StoredRow, error) {
|
||||||
|
rv := StoredRow{}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(key)
|
||||||
|
_, err := buf.ReadByte() // type
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.doc, err = buf.ReadBytes(ByteSeparator)
|
||||||
|
if len(rv.doc) < 2 { // 1 for min doc id length, 1 for separator
|
||||||
|
err = fmt.Errorf("invalid doc length 0")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.doc = rv.doc[:len(rv.doc)-1] // trim off separator byte
|
||||||
|
|
||||||
|
err = binary.Read(buf, binary.LittleEndian, &rv.field)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.arrayPositions = make([]uint64, 0)
|
||||||
|
nextArrayPos, err := binary.ReadUvarint(buf)
|
||||||
|
for err == nil {
|
||||||
|
rv.arrayPositions = append(rv.arrayPositions, nextArrayPos)
|
||||||
|
nextArrayPos, err = binary.ReadUvarint(buf)
|
||||||
|
}
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStoredRowKV(key, value []byte) (*StoredRow, error) {
|
||||||
|
rv, err := NewStoredRowK(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rv.typ = value[0]
|
||||||
|
rv.value = value[1:]
|
||||||
|
return rv, nil
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package upsidedown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mergeOperator upsideDownMerge
|
||||||
|
|
||||||
|
var dictionaryTermIncr []byte
|
||||||
|
var dictionaryTermDecr []byte
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dictionaryTermIncr = make([]byte, 8)
|
||||||
|
binary.LittleEndian.PutUint64(dictionaryTermIncr, uint64(1))
|
||||||
|
dictionaryTermDecr = make([]byte, 8)
|
||||||
|
var negOne = int64(-1)
|
||||||
|
binary.LittleEndian.PutUint64(dictionaryTermDecr, uint64(negOne))
|
||||||
|
}
|
||||||
|
|
||||||
|
type upsideDownMerge struct{}
|
||||||
|
|
||||||
|
func (m *upsideDownMerge) FullMerge(key, existingValue []byte, operands [][]byte) ([]byte, bool) {
|
||||||
|
// set up record based on key
|
||||||
|
dr, err := NewDictionaryRowK(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if len(existingValue) > 0 {
|
||||||
|
// if existing value, parse it
|
||||||
|
err = dr.parseDictionaryV(existingValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now process operands
|
||||||
|
for _, operand := range operands {
|
||||||
|
next := int64(binary.LittleEndian.Uint64(operand))
|
||||||
|
if next < 0 && uint64(-next) > dr.count {
|
||||||
|
// subtracting next from existing would overflow
|
||||||
|
dr.count = 0
|
||||||
|
} else if next < 0 {
|
||||||
|
dr.count -= uint64(-next)
|
||||||
|
} else {
|
||||||
|
dr.count += uint64(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dr.Value(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *upsideDownMerge) PartialMerge(key, leftOperand, rightOperand []byte) ([]byte, bool) {
|
||||||
|
left := int64(binary.LittleEndian.Uint64(leftOperand))
|
||||||
|
right := int64(binary.LittleEndian.Uint64(rightOperand))
|
||||||
|
rv := make([]byte, 8)
|
||||||
|
binary.LittleEndian.PutUint64(rv, uint64(left+right))
|
||||||
|
return rv, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *upsideDownMerge) Name() string {
|
||||||
|
return "upsideDownMerge"
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package upsidedown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type indexStat struct {
|
||||||
|
updates, deletes, batches, errors uint64
|
||||||
|
analysisTime, indexTime uint64
|
||||||
|
termSearchersStarted uint64
|
||||||
|
termSearchersFinished uint64
|
||||||
|
numPlainTextBytesIndexed uint64
|
||||||
|
i *UpsideDownCouch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexStat) statsMap() map[string]interface{} {
|
||||||
|
m := map[string]interface{}{}
|
||||||
|
m["updates"] = atomic.LoadUint64(&i.updates)
|
||||||
|
m["deletes"] = atomic.LoadUint64(&i.deletes)
|
||||||
|
m["batches"] = atomic.LoadUint64(&i.batches)
|
||||||
|
m["errors"] = atomic.LoadUint64(&i.errors)
|
||||||
|
m["analysis_time"] = atomic.LoadUint64(&i.analysisTime)
|
||||||
|
m["index_time"] = atomic.LoadUint64(&i.indexTime)
|
||||||
|
m["term_searchers_started"] = atomic.LoadUint64(&i.termSearchersStarted)
|
||||||
|
m["term_searchers_finished"] = atomic.LoadUint64(&i.termSearchersFinished)
|
||||||
|
m["num_plain_text_bytes_indexed"] = atomic.LoadUint64(&i.numPlainTextBytesIndexed)
|
||||||
|
|
||||||
|
if o, ok := i.i.store.(store.KVStoreStats); ok {
|
||||||
|
m["kv"] = o.StatsMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexStat) MarshalJSON() ([]byte, error) {
|
||||||
|
m := i.statsMap()
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
1037
vendor/github.com/blevesearch/bleve/index/upsidedown/upsidedown.go
generated
vendored
Normal file
1037
vendor/github.com/blevesearch/bleve/index/upsidedown/upsidedown.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
684
vendor/github.com/blevesearch/bleve/index/upsidedown/upsidedown.pb.go
generated
vendored
Normal file
684
vendor/github.com/blevesearch/bleve/index/upsidedown/upsidedown.pb.go
generated
vendored
Normal file
|
@ -0,0 +1,684 @@
|
||||||
|
// Code generated by protoc-gen-gogo.
|
||||||
|
// source: upsidedown.proto
|
||||||
|
// DO NOT EDIT!
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package upsidedown is a generated protocol buffer package.
|
||||||
|
|
||||||
|
It is generated from these files:
|
||||||
|
upsidedown.proto
|
||||||
|
|
||||||
|
It has these top-level messages:
|
||||||
|
BackIndexTermEntry
|
||||||
|
BackIndexStoreEntry
|
||||||
|
BackIndexRowValue
|
||||||
|
*/
|
||||||
|
package upsidedown
|
||||||
|
|
||||||
|
import proto "github.com/golang/protobuf/proto"
|
||||||
|
import math "math"
|
||||||
|
|
||||||
|
import io "io"
|
||||||
|
import fmt "fmt"
|
||||||
|
import github_com_golang_protobuf_proto "github.com/golang/protobuf/proto"
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
type BackIndexTermEntry struct {
|
||||||
|
Term *string `protobuf:"bytes,1,req,name=term" json:"term,omitempty"`
|
||||||
|
Field *uint32 `protobuf:"varint,2,req,name=field" json:"field,omitempty"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexTermEntry) Reset() { *m = BackIndexTermEntry{} }
|
||||||
|
func (m *BackIndexTermEntry) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*BackIndexTermEntry) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (m *BackIndexTermEntry) GetTerm() string {
|
||||||
|
if m != nil && m.Term != nil {
|
||||||
|
return *m.Term
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexTermEntry) GetField() uint32 {
|
||||||
|
if m != nil && m.Field != nil {
|
||||||
|
return *m.Field
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackIndexStoreEntry struct {
|
||||||
|
Field *uint32 `protobuf:"varint,1,req,name=field" json:"field,omitempty"`
|
||||||
|
ArrayPositions []uint64 `protobuf:"varint,2,rep,name=arrayPositions" json:"arrayPositions,omitempty"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexStoreEntry) Reset() { *m = BackIndexStoreEntry{} }
|
||||||
|
func (m *BackIndexStoreEntry) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*BackIndexStoreEntry) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (m *BackIndexStoreEntry) GetField() uint32 {
|
||||||
|
if m != nil && m.Field != nil {
|
||||||
|
return *m.Field
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexStoreEntry) GetArrayPositions() []uint64 {
|
||||||
|
if m != nil {
|
||||||
|
return m.ArrayPositions
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackIndexRowValue struct {
|
||||||
|
TermEntries []*BackIndexTermEntry `protobuf:"bytes,1,rep,name=termEntries" json:"termEntries,omitempty"`
|
||||||
|
StoredEntries []*BackIndexStoreEntry `protobuf:"bytes,2,rep,name=storedEntries" json:"storedEntries,omitempty"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexRowValue) Reset() { *m = BackIndexRowValue{} }
|
||||||
|
func (m *BackIndexRowValue) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*BackIndexRowValue) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (m *BackIndexRowValue) GetTermEntries() []*BackIndexTermEntry {
|
||||||
|
if m != nil {
|
||||||
|
return m.TermEntries
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexRowValue) GetStoredEntries() []*BackIndexStoreEntry {
|
||||||
|
if m != nil {
|
||||||
|
return m.StoredEntries
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexTermEntry) Unmarshal(data []byte) error {
|
||||||
|
var hasFields [1]uint64
|
||||||
|
l := len(data)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Term", wireType)
|
||||||
|
}
|
||||||
|
var stringLen uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
stringLen |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + int(stringLen)
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
s := string(data[iNdEx:postIndex])
|
||||||
|
m.Term = &s
|
||||||
|
iNdEx = postIndex
|
||||||
|
hasFields[0] |= uint64(0x00000001)
|
||||||
|
case 2:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Field", wireType)
|
||||||
|
}
|
||||||
|
var v uint32
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
v |= (uint32(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Field = &v
|
||||||
|
hasFields[0] |= uint64(0x00000002)
|
||||||
|
default:
|
||||||
|
var sizeOfWire int
|
||||||
|
for {
|
||||||
|
sizeOfWire++
|
||||||
|
wire >>= 7
|
||||||
|
if wire == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iNdEx -= sizeOfWire
|
||||||
|
skippy, err := skipUpsidedown(data[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if skippy < 0 {
|
||||||
|
return ErrInvalidLengthUpsidedown
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...)
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasFields[0]&uint64(0x00000001) == 0 {
|
||||||
|
return new(github_com_golang_protobuf_proto.RequiredNotSetError)
|
||||||
|
}
|
||||||
|
if hasFields[0]&uint64(0x00000002) == 0 {
|
||||||
|
return new(github_com_golang_protobuf_proto.RequiredNotSetError)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *BackIndexStoreEntry) Unmarshal(data []byte) error {
|
||||||
|
var hasFields [1]uint64
|
||||||
|
l := len(data)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Field", wireType)
|
||||||
|
}
|
||||||
|
var v uint32
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
v |= (uint32(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Field = &v
|
||||||
|
hasFields[0] |= uint64(0x00000001)
|
||||||
|
case 2:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field ArrayPositions", wireType)
|
||||||
|
}
|
||||||
|
var v uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
v |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.ArrayPositions = append(m.ArrayPositions, v)
|
||||||
|
default:
|
||||||
|
var sizeOfWire int
|
||||||
|
for {
|
||||||
|
sizeOfWire++
|
||||||
|
wire >>= 7
|
||||||
|
if wire == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iNdEx -= sizeOfWire
|
||||||
|
skippy, err := skipUpsidedown(data[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if skippy < 0 {
|
||||||
|
return ErrInvalidLengthUpsidedown
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...)
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasFields[0]&uint64(0x00000001) == 0 {
|
||||||
|
return new(github_com_golang_protobuf_proto.RequiredNotSetError)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *BackIndexRowValue) Unmarshal(data []byte) error {
|
||||||
|
l := len(data)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field TermEntries", wireType)
|
||||||
|
}
|
||||||
|
var msglen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
msglen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + msglen
|
||||||
|
if msglen < 0 {
|
||||||
|
return ErrInvalidLengthUpsidedown
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.TermEntries = append(m.TermEntries, &BackIndexTermEntry{})
|
||||||
|
if err := m.TermEntries[len(m.TermEntries)-1].Unmarshal(data[iNdEx:postIndex]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 2:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field StoredEntries", wireType)
|
||||||
|
}
|
||||||
|
var msglen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
msglen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + msglen
|
||||||
|
if msglen < 0 {
|
||||||
|
return ErrInvalidLengthUpsidedown
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.StoredEntries = append(m.StoredEntries, &BackIndexStoreEntry{})
|
||||||
|
if err := m.StoredEntries[len(m.StoredEntries)-1].Unmarshal(data[iNdEx:postIndex]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
default:
|
||||||
|
var sizeOfWire int
|
||||||
|
for {
|
||||||
|
sizeOfWire++
|
||||||
|
wire >>= 7
|
||||||
|
if wire == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iNdEx -= sizeOfWire
|
||||||
|
skippy, err := skipUpsidedown(data[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if skippy < 0 {
|
||||||
|
return ErrInvalidLengthUpsidedown
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...)
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func skipUpsidedown(data []byte) (n int, err error) {
|
||||||
|
l := len(data)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
switch wireType {
|
||||||
|
case 0:
|
||||||
|
for {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx++
|
||||||
|
if data[iNdEx-1] < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iNdEx, nil
|
||||||
|
case 1:
|
||||||
|
iNdEx += 8
|
||||||
|
return iNdEx, nil
|
||||||
|
case 2:
|
||||||
|
var length int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
length |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iNdEx += length
|
||||||
|
if length < 0 {
|
||||||
|
return 0, ErrInvalidLengthUpsidedown
|
||||||
|
}
|
||||||
|
return iNdEx, nil
|
||||||
|
case 3:
|
||||||
|
for {
|
||||||
|
var innerWire uint64
|
||||||
|
var start int = iNdEx
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := data[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
innerWire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
innerWireType := int(innerWire & 0x7)
|
||||||
|
if innerWireType == 4 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
next, err := skipUpsidedown(data[start:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
iNdEx = start + next
|
||||||
|
}
|
||||||
|
return iNdEx, nil
|
||||||
|
case 4:
|
||||||
|
return iNdEx, nil
|
||||||
|
case 5:
|
||||||
|
iNdEx += 4
|
||||||
|
return iNdEx, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidLengthUpsidedown = fmt.Errorf("proto: negative length found during unmarshaling")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *BackIndexTermEntry) Size() (n int) {
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.Term != nil {
|
||||||
|
l = len(*m.Term)
|
||||||
|
n += 1 + l + sovUpsidedown(uint64(l))
|
||||||
|
}
|
||||||
|
if m.Field != nil {
|
||||||
|
n += 1 + sovUpsidedown(uint64(*m.Field))
|
||||||
|
}
|
||||||
|
if m.XXX_unrecognized != nil {
|
||||||
|
n += len(m.XXX_unrecognized)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexStoreEntry) Size() (n int) {
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.Field != nil {
|
||||||
|
n += 1 + sovUpsidedown(uint64(*m.Field))
|
||||||
|
}
|
||||||
|
if len(m.ArrayPositions) > 0 {
|
||||||
|
for _, e := range m.ArrayPositions {
|
||||||
|
n += 1 + sovUpsidedown(uint64(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.XXX_unrecognized != nil {
|
||||||
|
n += len(m.XXX_unrecognized)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexRowValue) Size() (n int) {
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if len(m.TermEntries) > 0 {
|
||||||
|
for _, e := range m.TermEntries {
|
||||||
|
l = e.Size()
|
||||||
|
n += 1 + l + sovUpsidedown(uint64(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(m.StoredEntries) > 0 {
|
||||||
|
for _, e := range m.StoredEntries {
|
||||||
|
l = e.Size()
|
||||||
|
n += 1 + l + sovUpsidedown(uint64(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.XXX_unrecognized != nil {
|
||||||
|
n += len(m.XXX_unrecognized)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func sovUpsidedown(x uint64) (n int) {
|
||||||
|
for {
|
||||||
|
n++
|
||||||
|
x >>= 7
|
||||||
|
if x == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
func sozUpsidedown(x uint64) (n int) {
|
||||||
|
return sovUpsidedown(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
||||||
|
}
|
||||||
|
func (m *BackIndexTermEntry) Marshal() (data []byte, err error) {
|
||||||
|
size := m.Size()
|
||||||
|
data = make([]byte, size)
|
||||||
|
n, err := m.MarshalTo(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexTermEntry) MarshalTo(data []byte) (n int, err error) {
|
||||||
|
var i int
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.Term == nil {
|
||||||
|
return 0, new(github_com_golang_protobuf_proto.RequiredNotSetError)
|
||||||
|
} else {
|
||||||
|
data[i] = 0xa
|
||||||
|
i++
|
||||||
|
i = encodeVarintUpsidedown(data, i, uint64(len(*m.Term)))
|
||||||
|
i += copy(data[i:], *m.Term)
|
||||||
|
}
|
||||||
|
if m.Field == nil {
|
||||||
|
return 0, new(github_com_golang_protobuf_proto.RequiredNotSetError)
|
||||||
|
} else {
|
||||||
|
data[i] = 0x10
|
||||||
|
i++
|
||||||
|
i = encodeVarintUpsidedown(data, i, uint64(*m.Field))
|
||||||
|
}
|
||||||
|
if m.XXX_unrecognized != nil {
|
||||||
|
i += copy(data[i:], m.XXX_unrecognized)
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexStoreEntry) Marshal() (data []byte, err error) {
|
||||||
|
size := m.Size()
|
||||||
|
data = make([]byte, size)
|
||||||
|
n, err := m.MarshalTo(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexStoreEntry) MarshalTo(data []byte) (n int, err error) {
|
||||||
|
var i int
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.Field == nil {
|
||||||
|
return 0, new(github_com_golang_protobuf_proto.RequiredNotSetError)
|
||||||
|
} else {
|
||||||
|
data[i] = 0x8
|
||||||
|
i++
|
||||||
|
i = encodeVarintUpsidedown(data, i, uint64(*m.Field))
|
||||||
|
}
|
||||||
|
if len(m.ArrayPositions) > 0 {
|
||||||
|
for _, num := range m.ArrayPositions {
|
||||||
|
data[i] = 0x10
|
||||||
|
i++
|
||||||
|
i = encodeVarintUpsidedown(data, i, uint64(num))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.XXX_unrecognized != nil {
|
||||||
|
i += copy(data[i:], m.XXX_unrecognized)
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexRowValue) Marshal() (data []byte, err error) {
|
||||||
|
size := m.Size()
|
||||||
|
data = make([]byte, size)
|
||||||
|
n, err := m.MarshalTo(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackIndexRowValue) MarshalTo(data []byte) (n int, err error) {
|
||||||
|
var i int
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if len(m.TermEntries) > 0 {
|
||||||
|
for _, msg := range m.TermEntries {
|
||||||
|
data[i] = 0xa
|
||||||
|
i++
|
||||||
|
i = encodeVarintUpsidedown(data, i, uint64(msg.Size()))
|
||||||
|
n, err := msg.MarshalTo(data[i:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
i += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(m.StoredEntries) > 0 {
|
||||||
|
for _, msg := range m.StoredEntries {
|
||||||
|
data[i] = 0x12
|
||||||
|
i++
|
||||||
|
i = encodeVarintUpsidedown(data, i, uint64(msg.Size()))
|
||||||
|
n, err := msg.MarshalTo(data[i:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
i += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.XXX_unrecognized != nil {
|
||||||
|
i += copy(data[i:], m.XXX_unrecognized)
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeFixed64Upsidedown(data []byte, offset int, v uint64) int {
|
||||||
|
data[offset] = uint8(v)
|
||||||
|
data[offset+1] = uint8(v >> 8)
|
||||||
|
data[offset+2] = uint8(v >> 16)
|
||||||
|
data[offset+3] = uint8(v >> 24)
|
||||||
|
data[offset+4] = uint8(v >> 32)
|
||||||
|
data[offset+5] = uint8(v >> 40)
|
||||||
|
data[offset+6] = uint8(v >> 48)
|
||||||
|
data[offset+7] = uint8(v >> 56)
|
||||||
|
return offset + 8
|
||||||
|
}
|
||||||
|
func encodeFixed32Upsidedown(data []byte, offset int, v uint32) int {
|
||||||
|
data[offset] = uint8(v)
|
||||||
|
data[offset+1] = uint8(v >> 8)
|
||||||
|
data[offset+2] = uint8(v >> 16)
|
||||||
|
data[offset+3] = uint8(v >> 24)
|
||||||
|
return offset + 4
|
||||||
|
}
|
||||||
|
func encodeVarintUpsidedown(data []byte, offset int, v uint64) int {
|
||||||
|
for v >= 1<<7 {
|
||||||
|
data[offset] = uint8(v&0x7f | 0x80)
|
||||||
|
v >>= 7
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
data[offset] = uint8(v)
|
||||||
|
return offset + 1
|
||||||
|
}
|
14
vendor/github.com/blevesearch/bleve/index/upsidedown/upsidedown.proto
generated
vendored
Normal file
14
vendor/github.com/blevesearch/bleve/index/upsidedown/upsidedown.proto
generated
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
message BackIndexTermEntry {
|
||||||
|
required string term = 1;
|
||||||
|
required uint32 field = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BackIndexStoreEntry {
|
||||||
|
required uint32 field = 1;
|
||||||
|
repeated uint64 arrayPositions = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BackIndexRowValue {
|
||||||
|
repeated BackIndexTermEntry termEntries = 1;
|
||||||
|
repeated BackIndexStoreEntry storedEntries = 2;
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
// An IndexAlias is a wrapper around one or more
|
||||||
|
// Index objects. It has two distinct modes of
|
||||||
|
// operation.
|
||||||
|
// 1. When it points to a single index, ALL index
|
||||||
|
// operations are valid and will be passed through
|
||||||
|
// to the underlying index.
|
||||||
|
// 2. When it points to more than one index, the only
|
||||||
|
// valid operation is Search. In this case the
|
||||||
|
// search will be performed across all the
|
||||||
|
// underlying indexes and the results merged.
|
||||||
|
// Calls to Add/Remove/Swap the underlying indexes
|
||||||
|
// are atomic, so you can safely change the
|
||||||
|
// underlying Index objects while other components
|
||||||
|
// are performing operations.
|
||||||
|
type IndexAlias interface {
|
||||||
|
Index
|
||||||
|
|
||||||
|
Add(i ...Index)
|
||||||
|
Remove(i ...Index)
|
||||||
|
Swap(in, out []Index)
|
||||||
|
}
|
|
@ -0,0 +1,605 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/document"
|
||||||
|
"github.com/blevesearch/bleve/index"
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
"github.com/blevesearch/bleve/mapping"
|
||||||
|
"github.com/blevesearch/bleve/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
type indexAliasImpl struct {
|
||||||
|
name string
|
||||||
|
indexes []Index
|
||||||
|
mutex sync.RWMutex
|
||||||
|
open bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIndexAlias creates a new IndexAlias over the provided
|
||||||
|
// Index objects.
|
||||||
|
func NewIndexAlias(indexes ...Index) *indexAliasImpl {
|
||||||
|
return &indexAliasImpl{
|
||||||
|
name: "alias",
|
||||||
|
indexes: indexes,
|
||||||
|
open: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) isAliasToSingleIndex() error {
|
||||||
|
if len(i.indexes) < 1 {
|
||||||
|
return ErrorAliasEmpty
|
||||||
|
} else if len(i.indexes) > 1 {
|
||||||
|
return ErrorAliasMulti
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Index(id string, data interface{}) error {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].Index(id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Delete(id string) error {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Batch(b *Batch) error {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].Batch(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Document(id string) (*document.Document, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].Document(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) DocCount() (uint64, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
rv := uint64(0)
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return 0, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, index := range i.indexes {
|
||||||
|
otherCount, err := index.DocCount()
|
||||||
|
if err == nil {
|
||||||
|
rv += otherCount
|
||||||
|
}
|
||||||
|
// tolerate errors to produce partial counts
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Search(req *SearchRequest) (*SearchResult, error) {
|
||||||
|
return i.SearchInContext(context.Background(), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) SearchInContext(ctx context.Context, req *SearchRequest) (*SearchResult, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(i.indexes) < 1 {
|
||||||
|
return nil, ErrorAliasEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
// short circuit the simple case
|
||||||
|
if len(i.indexes) == 1 {
|
||||||
|
return i.indexes[0].SearchInContext(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MultiSearch(ctx, req, i.indexes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Fields() ([]string, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].Fields()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) FieldDict(field string) (index.FieldDict, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldDict, err := i.indexes[0].FieldDict(field)
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &indexAliasImplFieldDict{
|
||||||
|
index: i,
|
||||||
|
fieldDict: fieldDict,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldDict, err := i.indexes[0].FieldDictRange(field, startTerm, endTerm)
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &indexAliasImplFieldDict{
|
||||||
|
index: i,
|
||||||
|
fieldDict: fieldDict,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldDict, err := i.indexes[0].FieldDictPrefix(field, termPrefix)
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &indexAliasImplFieldDict{
|
||||||
|
index: i,
|
||||||
|
fieldDict: fieldDict,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Close() error {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
|
||||||
|
i.open = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Mapping() mapping.IndexMapping {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].Mapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Stats() *IndexStat {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].Stats()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) StatsMap() map[string]interface{} {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].StatsMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) GetInternal(key []byte) ([]byte, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].GetInternal(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) SetInternal(key, val []byte) error {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].SetInternal(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) DeleteInternal(key []byte) error {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].DeleteInternal(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Advanced() (index.Index, store.KVStore, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil, nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].Advanced()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Add(indexes ...Index) {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
|
||||||
|
i.indexes = append(i.indexes, indexes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) removeSingle(index Index) {
|
||||||
|
for pos, in := range i.indexes {
|
||||||
|
if in == index {
|
||||||
|
i.indexes = append(i.indexes[:pos], i.indexes[pos+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Remove(indexes ...Index) {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
|
||||||
|
for _, in := range indexes {
|
||||||
|
i.removeSingle(in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Swap(in, out []Index) {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
|
||||||
|
// add
|
||||||
|
i.indexes = append(i.indexes, in...)
|
||||||
|
|
||||||
|
// delete
|
||||||
|
for _, ind := range out {
|
||||||
|
i.removeSingle(ind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createChildSearchRequest creates a separate
|
||||||
|
// request from the original
|
||||||
|
// For now, avoid data race on req structure.
|
||||||
|
// TODO disable highlight/field load on child
|
||||||
|
// requests, and add code to do this only on
|
||||||
|
// the actual final results.
|
||||||
|
// Perhaps that part needs to be optional,
|
||||||
|
// could be slower in remote usages.
|
||||||
|
func createChildSearchRequest(req *SearchRequest) *SearchRequest {
|
||||||
|
rv := SearchRequest{
|
||||||
|
Query: req.Query,
|
||||||
|
Size: req.Size + req.From,
|
||||||
|
From: 0,
|
||||||
|
Highlight: req.Highlight,
|
||||||
|
Fields: req.Fields,
|
||||||
|
Facets: req.Facets,
|
||||||
|
Explain: req.Explain,
|
||||||
|
Sort: req.Sort,
|
||||||
|
}
|
||||||
|
return &rv
|
||||||
|
}
|
||||||
|
|
||||||
|
type asyncSearchResult struct {
|
||||||
|
Name string
|
||||||
|
Result *SearchResult
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiSearch executes a SearchRequest across multiple Index objects,
|
||||||
|
// then merges the results. The indexes must honor any ctx deadline.
|
||||||
|
func MultiSearch(ctx context.Context, req *SearchRequest, indexes ...Index) (*SearchResult, error) {
|
||||||
|
|
||||||
|
searchStart := time.Now()
|
||||||
|
asyncResults := make(chan *asyncSearchResult, len(indexes))
|
||||||
|
|
||||||
|
// run search on each index in separate go routine
|
||||||
|
var waitGroup sync.WaitGroup
|
||||||
|
|
||||||
|
var searchChildIndex = func(in Index, childReq *SearchRequest) {
|
||||||
|
rv := asyncSearchResult{Name: in.Name()}
|
||||||
|
rv.Result, rv.Err = in.SearchInContext(ctx, childReq)
|
||||||
|
asyncResults <- &rv
|
||||||
|
waitGroup.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitGroup.Add(len(indexes))
|
||||||
|
for _, in := range indexes {
|
||||||
|
go searchChildIndex(in, createChildSearchRequest(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
// on another go routine, close after finished
|
||||||
|
go func() {
|
||||||
|
waitGroup.Wait()
|
||||||
|
close(asyncResults)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var sr *SearchResult
|
||||||
|
indexErrors := make(map[string]error)
|
||||||
|
|
||||||
|
for asr := range asyncResults {
|
||||||
|
if asr.Err == nil {
|
||||||
|
if sr == nil {
|
||||||
|
// first result
|
||||||
|
sr = asr.Result
|
||||||
|
} else {
|
||||||
|
// merge with previous
|
||||||
|
sr.Merge(asr.Result)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
indexErrors[asr.Name] = asr.Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge just concatenated all the hits
|
||||||
|
// now lets clean it up
|
||||||
|
|
||||||
|
// handle case where no results were successful
|
||||||
|
if sr == nil {
|
||||||
|
sr = &SearchResult{
|
||||||
|
Status: &SearchStatus{
|
||||||
|
Errors: make(map[string]error),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort all hits with the requested order
|
||||||
|
if len(req.Sort) > 0 {
|
||||||
|
sorter := newMultiSearchHitSorter(req.Sort, sr.Hits)
|
||||||
|
sort.Sort(sorter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now skip over the correct From
|
||||||
|
if req.From > 0 && len(sr.Hits) > req.From {
|
||||||
|
sr.Hits = sr.Hits[req.From:]
|
||||||
|
} else if req.From > 0 {
|
||||||
|
sr.Hits = search.DocumentMatchCollection{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now trim to the correct size
|
||||||
|
if req.Size > 0 && len(sr.Hits) > req.Size {
|
||||||
|
sr.Hits = sr.Hits[0:req.Size]
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix up facets
|
||||||
|
for name, fr := range req.Facets {
|
||||||
|
sr.Facets.Fixup(name, fr.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix up original request
|
||||||
|
sr.Request = req
|
||||||
|
searchDuration := time.Since(searchStart)
|
||||||
|
sr.Took = searchDuration
|
||||||
|
|
||||||
|
// fix up errors
|
||||||
|
if len(indexErrors) > 0 {
|
||||||
|
if sr.Status.Errors == nil {
|
||||||
|
sr.Status.Errors = make(map[string]error)
|
||||||
|
}
|
||||||
|
for indexName, indexErr := range indexErrors {
|
||||||
|
sr.Status.Errors[indexName] = indexErr
|
||||||
|
sr.Status.Total++
|
||||||
|
sr.Status.Failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) NewBatch() *Batch {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.isAliasToSingleIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.indexes[0].NewBatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) Name() string {
|
||||||
|
return i.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexAliasImpl) SetName(name string) {
|
||||||
|
i.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexAliasImplFieldDict struct {
|
||||||
|
index *indexAliasImpl
|
||||||
|
fieldDict index.FieldDict
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *indexAliasImplFieldDict) Next() (*index.DictEntry, error) {
|
||||||
|
return f.fieldDict.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *indexAliasImplFieldDict) Close() error {
|
||||||
|
defer f.index.mutex.RUnlock()
|
||||||
|
return f.fieldDict.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type multiSearchHitSorter struct {
|
||||||
|
hits search.DocumentMatchCollection
|
||||||
|
sort search.SortOrder
|
||||||
|
cachedScoring []bool
|
||||||
|
cachedDesc []bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMultiSearchHitSorter(sort search.SortOrder, hits search.DocumentMatchCollection) *multiSearchHitSorter {
|
||||||
|
return &multiSearchHitSorter{
|
||||||
|
sort: sort,
|
||||||
|
hits: hits,
|
||||||
|
cachedScoring: sort.CacheIsScore(),
|
||||||
|
cachedDesc: sort.CacheDescending(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiSearchHitSorter) Len() int { return len(m.hits) }
|
||||||
|
func (m *multiSearchHitSorter) Swap(i, j int) { m.hits[i], m.hits[j] = m.hits[j], m.hits[i] }
|
||||||
|
func (m *multiSearchHitSorter) Less(i, j int) bool {
|
||||||
|
c := m.sort.Compare(m.cachedScoring, m.cachedDesc, m.hits[i], m.hits[j])
|
||||||
|
return c < 0
|
||||||
|
}
|
|
@ -0,0 +1,729 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/document"
|
||||||
|
"github.com/blevesearch/bleve/index"
|
||||||
|
"github.com/blevesearch/bleve/index/store"
|
||||||
|
"github.com/blevesearch/bleve/index/upsidedown"
|
||||||
|
"github.com/blevesearch/bleve/mapping"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
"github.com/blevesearch/bleve/search"
|
||||||
|
"github.com/blevesearch/bleve/search/collector"
|
||||||
|
"github.com/blevesearch/bleve/search/facet"
|
||||||
|
"github.com/blevesearch/bleve/search/highlight"
|
||||||
|
)
|
||||||
|
|
||||||
|
type indexImpl struct {
|
||||||
|
path string
|
||||||
|
name string
|
||||||
|
meta *indexMeta
|
||||||
|
i index.Index
|
||||||
|
m mapping.IndexMapping
|
||||||
|
mutex sync.RWMutex
|
||||||
|
open bool
|
||||||
|
stats *IndexStat
|
||||||
|
}
|
||||||
|
|
||||||
|
const storePath = "store"
|
||||||
|
|
||||||
|
var mappingInternalKey = []byte("_mapping")
|
||||||
|
|
||||||
|
func indexStorePath(path string) string {
|
||||||
|
return path + string(os.PathSeparator) + storePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIndexUsing(path string, mapping mapping.IndexMapping, indexType string, kvstore string, kvconfig map[string]interface{}) (*indexImpl, error) {
|
||||||
|
// first validate the mapping
|
||||||
|
err := mapping.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if kvconfig == nil {
|
||||||
|
kvconfig = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if kvstore == "" {
|
||||||
|
return nil, fmt.Errorf("bleve not configured for file based indexing")
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := indexImpl{
|
||||||
|
path: path,
|
||||||
|
name: path,
|
||||||
|
m: mapping,
|
||||||
|
meta: newIndexMeta(indexType, kvstore, kvconfig),
|
||||||
|
}
|
||||||
|
rv.stats = &IndexStat{i: &rv}
|
||||||
|
// at this point there is hope that we can be successful, so save index meta
|
||||||
|
if path != "" {
|
||||||
|
err = rv.meta.Save(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kvconfig["create_if_missing"] = true
|
||||||
|
kvconfig["error_if_exists"] = true
|
||||||
|
kvconfig["path"] = indexStorePath(path)
|
||||||
|
} else {
|
||||||
|
kvconfig["path"] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the index
|
||||||
|
indexTypeConstructor := registry.IndexTypeConstructorByName(rv.meta.IndexType)
|
||||||
|
if indexTypeConstructor == nil {
|
||||||
|
return nil, ErrorUnknownIndexType
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.i, err = indexTypeConstructor(rv.meta.Storage, kvconfig, Config.analysisQueue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = rv.i.Open()
|
||||||
|
if err != nil {
|
||||||
|
if err == index.ErrorUnknownStorageType {
|
||||||
|
return nil, ErrorUnknownStorageType
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// now persist the mapping
|
||||||
|
mappingBytes, err := json.Marshal(mapping)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = rv.i.SetInternal(mappingInternalKey, mappingBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark the index as open
|
||||||
|
rv.mutex.Lock()
|
||||||
|
defer rv.mutex.Unlock()
|
||||||
|
rv.open = true
|
||||||
|
indexStats.Register(&rv)
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openIndexUsing(path string, runtimeConfig map[string]interface{}) (rv *indexImpl, err error) {
|
||||||
|
rv = &indexImpl{
|
||||||
|
path: path,
|
||||||
|
name: path,
|
||||||
|
}
|
||||||
|
rv.stats = &IndexStat{i: rv}
|
||||||
|
|
||||||
|
rv.meta, err = openIndexMeta(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// backwards compatibility if index type is missing
|
||||||
|
if rv.meta.IndexType == "" {
|
||||||
|
rv.meta.IndexType = upsidedown.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
storeConfig := rv.meta.Config
|
||||||
|
if storeConfig == nil {
|
||||||
|
storeConfig = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
storeConfig["path"] = indexStorePath(path)
|
||||||
|
storeConfig["create_if_missing"] = false
|
||||||
|
storeConfig["error_if_exists"] = false
|
||||||
|
for rck, rcv := range runtimeConfig {
|
||||||
|
storeConfig[rck] = rcv
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the index
|
||||||
|
indexTypeConstructor := registry.IndexTypeConstructorByName(rv.meta.IndexType)
|
||||||
|
if indexTypeConstructor == nil {
|
||||||
|
return nil, ErrorUnknownIndexType
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.i, err = indexTypeConstructor(rv.meta.Storage, storeConfig, Config.analysisQueue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = rv.i.Open()
|
||||||
|
if err != nil {
|
||||||
|
if err == index.ErrorUnknownStorageType {
|
||||||
|
return nil, ErrorUnknownStorageType
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// now load the mapping
|
||||||
|
indexReader, err := rv.i.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := indexReader.Close(); cerr != nil && err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
mappingBytes, err := indexReader.GetInternal(mappingInternalKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var im *mapping.IndexMappingImpl
|
||||||
|
err = json.Unmarshal(mappingBytes, &im)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing mapping JSON: %v\nmapping contents:\n%s", err, string(mappingBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark the index as open
|
||||||
|
rv.mutex.Lock()
|
||||||
|
defer rv.mutex.Unlock()
|
||||||
|
rv.open = true
|
||||||
|
|
||||||
|
// validate the mapping
|
||||||
|
err = im.Validate()
|
||||||
|
if err != nil {
|
||||||
|
// note even if the mapping is invalid
|
||||||
|
// we still return an open usable index
|
||||||
|
return rv, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.m = im
|
||||||
|
indexStats.Register(rv)
|
||||||
|
return rv, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced returns implementation internals
|
||||||
|
// necessary ONLY for advanced usage.
|
||||||
|
func (i *indexImpl) Advanced() (index.Index, store.KVStore, error) {
|
||||||
|
s, err := i.i.Advanced()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return i.i, s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping returns the IndexMapping in use by this
|
||||||
|
// Index.
|
||||||
|
func (i *indexImpl) Mapping() mapping.IndexMapping {
|
||||||
|
return i.m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index the object with the specified identifier.
|
||||||
|
// The IndexMapping for this index will determine
|
||||||
|
// how the object is indexed.
|
||||||
|
func (i *indexImpl) Index(id string, data interface{}) (err error) {
|
||||||
|
if id == "" {
|
||||||
|
return ErrorEmptyID
|
||||||
|
}
|
||||||
|
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := document.NewDocument(id)
|
||||||
|
err = i.m.MapDocument(doc, data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = i.i.Update(doc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete entries for the specified identifier from
|
||||||
|
// the index.
|
||||||
|
func (i *indexImpl) Delete(id string) (err error) {
|
||||||
|
if id == "" {
|
||||||
|
return ErrorEmptyID
|
||||||
|
}
|
||||||
|
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.i.Delete(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch executes multiple Index and Delete
|
||||||
|
// operations at the same time. There are often
|
||||||
|
// significant performance benefits when performing
|
||||||
|
// operations in a batch.
|
||||||
|
func (i *indexImpl) Batch(b *Batch) error {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.i.Batch(b.internal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document is used to find the values of all the
|
||||||
|
// stored fields for a document in the index. These
|
||||||
|
// stored fields are put back into a Document object
|
||||||
|
// and returned.
|
||||||
|
func (i *indexImpl) Document(id string) (doc *document.Document, err error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
indexReader, err := i.i.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := indexReader.Close(); err == nil && cerr != nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
doc, err = indexReader.Document(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return doc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocCount returns the number of documents in the
|
||||||
|
// index.
|
||||||
|
func (i *indexImpl) DocCount() (count uint64, err error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return 0, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
// open a reader for this search
|
||||||
|
indexReader, err := i.i.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("error opening index reader %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := indexReader.Close(); err == nil && cerr != nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
count, err = indexReader.DocCount()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search executes a search request operation.
|
||||||
|
// Returns a SearchResult object or an error.
|
||||||
|
func (i *indexImpl) Search(req *SearchRequest) (sr *SearchResult, err error) {
|
||||||
|
return i.SearchInContext(context.Background(), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchInContext executes a search request operation within the provided
|
||||||
|
// Context. Returns a SearchResult object or an error.
|
||||||
|
func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr *SearchResult, err error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
searchStart := time.Now()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
collector := collector.NewTopNCollector(req.Size, req.From, req.Sort)
|
||||||
|
|
||||||
|
// open a reader for this search
|
||||||
|
indexReader, err := i.i.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error opening index reader %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := indexReader.Close(); err == nil && cerr != nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
searcher, err := req.Query.Searcher(indexReader, i.m, req.Explain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if serr := searcher.Close(); err == nil && serr != nil {
|
||||||
|
err = serr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if req.Facets != nil {
|
||||||
|
facetsBuilder := search.NewFacetsBuilder(indexReader)
|
||||||
|
for facetName, facetRequest := range req.Facets {
|
||||||
|
if facetRequest.NumericRanges != nil {
|
||||||
|
// build numeric range facet
|
||||||
|
facetBuilder := facet.NewNumericFacetBuilder(facetRequest.Field, facetRequest.Size)
|
||||||
|
for _, nr := range facetRequest.NumericRanges {
|
||||||
|
facetBuilder.AddRange(nr.Name, nr.Min, nr.Max)
|
||||||
|
}
|
||||||
|
facetsBuilder.Add(facetName, facetBuilder)
|
||||||
|
} else if facetRequest.DateTimeRanges != nil {
|
||||||
|
// build date range facet
|
||||||
|
facetBuilder := facet.NewDateTimeFacetBuilder(facetRequest.Field, facetRequest.Size)
|
||||||
|
dateTimeParser := i.m.DateTimeParserNamed("")
|
||||||
|
for _, dr := range facetRequest.DateTimeRanges {
|
||||||
|
start, end := dr.ParseDates(dateTimeParser)
|
||||||
|
facetBuilder.AddRange(dr.Name, start, end)
|
||||||
|
}
|
||||||
|
facetsBuilder.Add(facetName, facetBuilder)
|
||||||
|
} else {
|
||||||
|
// build terms facet
|
||||||
|
facetBuilder := facet.NewTermsFacetBuilder(facetRequest.Field, facetRequest.Size)
|
||||||
|
facetsBuilder.Add(facetName, facetBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collector.SetFacetsBuilder(facetsBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collector.Collect(ctx, searcher, indexReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hits := collector.Results()
|
||||||
|
|
||||||
|
var highlighter highlight.Highlighter
|
||||||
|
|
||||||
|
if req.Highlight != nil {
|
||||||
|
// get the right highlighter
|
||||||
|
highlighter, err = Config.Cache.HighlighterNamed(Config.DefaultHighlighter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.Highlight.Style != nil {
|
||||||
|
highlighter, err = Config.Cache.HighlighterNamed(*req.Highlight.Style)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if highlighter == nil {
|
||||||
|
return nil, fmt.Errorf("no highlighter named `%s` registered", *req.Highlight.Style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hit := range hits {
|
||||||
|
if len(req.Fields) > 0 || highlighter != nil {
|
||||||
|
doc, err := indexReader.Document(hit.ID)
|
||||||
|
if err == nil && doc != nil {
|
||||||
|
if len(req.Fields) > 0 {
|
||||||
|
for _, f := range req.Fields {
|
||||||
|
for _, docF := range doc.Fields {
|
||||||
|
if f == "*" || docF.Name() == f {
|
||||||
|
var value interface{}
|
||||||
|
switch docF := docF.(type) {
|
||||||
|
case *document.TextField:
|
||||||
|
value = string(docF.Value())
|
||||||
|
case *document.NumericField:
|
||||||
|
num, err := docF.Number()
|
||||||
|
if err == nil {
|
||||||
|
value = num
|
||||||
|
}
|
||||||
|
case *document.DateTimeField:
|
||||||
|
datetime, err := docF.DateTime()
|
||||||
|
if err == nil {
|
||||||
|
value = datetime.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
case *document.BooleanField:
|
||||||
|
boolean, err := docF.Boolean()
|
||||||
|
if err == nil {
|
||||||
|
value = boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value != nil {
|
||||||
|
hit.AddFieldValue(docF.Name(), value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if highlighter != nil {
|
||||||
|
highlightFields := req.Highlight.Fields
|
||||||
|
if highlightFields == nil {
|
||||||
|
// add all fields with matches
|
||||||
|
highlightFields = make([]string, 0, len(hit.Locations))
|
||||||
|
for k := range hit.Locations {
|
||||||
|
highlightFields = append(highlightFields, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, hf := range highlightFields {
|
||||||
|
highlighter.BestFragmentsInField(hit, doc, hf, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if doc == nil {
|
||||||
|
// unexpected case, a doc ID that was found as a search hit
|
||||||
|
// was unable to be found during document lookup
|
||||||
|
return nil, ErrorIndexReadInconsistency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i.name != "" {
|
||||||
|
hit.Index = i.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddUint64(&i.stats.searches, 1)
|
||||||
|
searchDuration := time.Since(searchStart)
|
||||||
|
atomic.AddUint64(&i.stats.searchTime, uint64(searchDuration))
|
||||||
|
|
||||||
|
if Config.SlowSearchLogThreshold > 0 &&
|
||||||
|
searchDuration > Config.SlowSearchLogThreshold {
|
||||||
|
logger.Printf("slow search took %s - %v", searchDuration, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SearchResult{
|
||||||
|
Status: &SearchStatus{
|
||||||
|
Total: 1,
|
||||||
|
Failed: 0,
|
||||||
|
Successful: 1,
|
||||||
|
Errors: make(map[string]error),
|
||||||
|
},
|
||||||
|
Request: req,
|
||||||
|
Hits: hits,
|
||||||
|
Total: collector.Total(),
|
||||||
|
MaxScore: collector.MaxScore(),
|
||||||
|
Took: searchDuration,
|
||||||
|
Facets: collector.FacetResults(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields returns the name of all the fields this
|
||||||
|
// Index has operated on.
|
||||||
|
func (i *indexImpl) Fields() (fields []string, err error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
indexReader, err := i.i.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := indexReader.Close(); err == nil && cerr != nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fields, err = indexReader.Fields()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) FieldDict(field string) (index.FieldDict, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
indexReader, err := i.i.Reader()
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldDict, err := indexReader.FieldDict(field)
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &indexImplFieldDict{
|
||||||
|
index: i,
|
||||||
|
indexReader: indexReader,
|
||||||
|
fieldDict: fieldDict,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
indexReader, err := i.i.Reader()
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldDict, err := indexReader.FieldDictRange(field, startTerm, endTerm)
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &indexImplFieldDict{
|
||||||
|
index: i,
|
||||||
|
indexReader: indexReader,
|
||||||
|
fieldDict: fieldDict,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
indexReader, err := i.i.Reader()
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldDict, err := indexReader.FieldDictPrefix(field, termPrefix)
|
||||||
|
if err != nil {
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &indexImplFieldDict{
|
||||||
|
index: i,
|
||||||
|
indexReader: indexReader,
|
||||||
|
fieldDict: fieldDict,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) Close() error {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
|
||||||
|
indexStats.UnRegister(i)
|
||||||
|
|
||||||
|
i.open = false
|
||||||
|
return i.i.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) Stats() *IndexStat {
|
||||||
|
return i.stats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) StatsMap() map[string]interface{} {
|
||||||
|
return i.stats.statsMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) GetInternal(key []byte) (val []byte, err error) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return nil, ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := i.i.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := reader.Close(); err == nil && cerr != nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
val, err = reader.GetInternal(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) SetInternal(key, val []byte) error {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.i.SetInternal(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) DeleteInternal(key []byte) error {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !i.open {
|
||||||
|
return ErrorIndexClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.i.DeleteInternal(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBatch creates a new empty batch.
|
||||||
|
func (i *indexImpl) NewBatch() *Batch {
|
||||||
|
return &Batch{
|
||||||
|
index: i,
|
||||||
|
internal: index.NewBatch(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) Name() string {
|
||||||
|
return i.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexImpl) SetName(name string) {
|
||||||
|
indexStats.UnRegister(i)
|
||||||
|
i.name = name
|
||||||
|
indexStats.Register(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexImplFieldDict struct {
|
||||||
|
index *indexImpl
|
||||||
|
indexReader index.IndexReader
|
||||||
|
fieldDict index.FieldDict
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *indexImplFieldDict) Next() (*index.DictEntry, error) {
|
||||||
|
return f.fieldDict.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *indexImplFieldDict) Close() error {
|
||||||
|
defer f.index.mutex.RUnlock()
|
||||||
|
err := f.fieldDict.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.indexReader.Close()
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/index/upsidedown"
|
||||||
|
)
|
||||||
|
|
||||||
|
const metaFilename = "index_meta.json"
|
||||||
|
|
||||||
|
type indexMeta struct {
|
||||||
|
Storage string `json:"storage"`
|
||||||
|
IndexType string `json:"index_type"`
|
||||||
|
Config map[string]interface{} `json:"config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIndexMeta(indexType string, storage string, config map[string]interface{}) *indexMeta {
|
||||||
|
return &indexMeta{
|
||||||
|
IndexType: indexType,
|
||||||
|
Storage: storage,
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func openIndexMeta(path string) (*indexMeta, error) {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return nil, ErrorIndexPathDoesNotExist
|
||||||
|
}
|
||||||
|
indexMetaPath := indexMetaPath(path)
|
||||||
|
metaBytes, err := ioutil.ReadFile(indexMetaPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrorIndexMetaMissing
|
||||||
|
}
|
||||||
|
var im indexMeta
|
||||||
|
err = json.Unmarshal(metaBytes, &im)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrorIndexMetaCorrupt
|
||||||
|
}
|
||||||
|
if im.IndexType == "" {
|
||||||
|
im.IndexType = upsidedown.Name
|
||||||
|
}
|
||||||
|
return &im, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexMeta) Save(path string) (err error) {
|
||||||
|
indexMetaPath := indexMetaPath(path)
|
||||||
|
// ensure any necessary parent directories exist
|
||||||
|
err = os.MkdirAll(path, 0700)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsExist(err) {
|
||||||
|
return ErrorIndexPathExists
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
metaBytes, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
indexMetaFile, err := os.OpenFile(indexMetaPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsExist(err) {
|
||||||
|
return ErrorIndexPathExists
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if ierr := indexMetaFile.Close(); err == nil && ierr != nil {
|
||||||
|
err = ierr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err = indexMetaFile.Write(metaBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexMetaPath(path string) string {
|
||||||
|
return path + string(os.PathSeparator) + metaFilename
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexStat struct {
|
||||||
|
searches uint64
|
||||||
|
searchTime uint64
|
||||||
|
i *indexImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *IndexStat) statsMap() map[string]interface{} {
|
||||||
|
m := map[string]interface{}{}
|
||||||
|
m["index"] = is.i.i.StatsMap()
|
||||||
|
m["searches"] = atomic.LoadUint64(&is.searches)
|
||||||
|
m["search_time"] = atomic.LoadUint64(&is.searchTime)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *IndexStat) MarshalJSON() ([]byte, error) {
|
||||||
|
m := is.statsMap()
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexStats struct {
|
||||||
|
indexes map[string]*IndexStat
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIndexStats() *IndexStats {
|
||||||
|
return &IndexStats{
|
||||||
|
indexes: make(map[string]*IndexStat),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexStats) Register(index Index) {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
i.indexes[index.Name()] = index.Stats()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexStats) UnRegister(index Index) {
|
||||||
|
i.mutex.Lock()
|
||||||
|
defer i.mutex.Unlock()
|
||||||
|
delete(i.indexes, index.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IndexStats) String() string {
|
||||||
|
i.mutex.RLock()
|
||||||
|
defer i.mutex.RUnlock()
|
||||||
|
bytes, err := json.Marshal(i.indexes)
|
||||||
|
if err != nil {
|
||||||
|
return "error marshaling stats"
|
||||||
|
}
|
||||||
|
return string(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexStats *IndexStats
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
import "github.com/blevesearch/bleve/mapping"
|
||||||
|
|
||||||
|
// NewIndexMapping creates a new IndexMapping that will use all the default indexing rules
|
||||||
|
func NewIndexMapping() *mapping.IndexMappingImpl {
|
||||||
|
return mapping.NewIndexMapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentMapping returns a new document mapping
|
||||||
|
// with all the default values.
|
||||||
|
func NewDocumentMapping() *mapping.DocumentMapping {
|
||||||
|
return mapping.NewDocumentMapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentStaticMapping returns a new document
|
||||||
|
// mapping that will not automatically index parts
|
||||||
|
// of a document without an explicit mapping.
|
||||||
|
func NewDocumentStaticMapping() *mapping.DocumentMapping {
|
||||||
|
return mapping.NewDocumentStaticMapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentDisabledMapping returns a new document
|
||||||
|
// mapping that will not perform any indexing.
|
||||||
|
func NewDocumentDisabledMapping() *mapping.DocumentMapping {
|
||||||
|
return mapping.NewDocumentDisabledMapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextFieldMapping returns a default field mapping for text
|
||||||
|
func NewTextFieldMapping() *mapping.FieldMapping {
|
||||||
|
return mapping.NewTextFieldMapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNumericFieldMapping returns a default field mapping for numbers
|
||||||
|
func NewNumericFieldMapping() *mapping.FieldMapping {
|
||||||
|
return mapping.NewNumericFieldMapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDateTimeFieldMapping returns a default field mapping for dates
|
||||||
|
func NewDateTimeFieldMapping() *mapping.FieldMapping {
|
||||||
|
return mapping.NewDateTimeFieldMapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBooleanFieldMapping returns a default field mapping for booleans
|
||||||
|
func NewBooleanFieldMapping() *mapping.FieldMapping {
|
||||||
|
return mapping.NewBooleanFieldMapping()
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mapping
|
||||||
|
|
||||||
|
type customAnalysis struct {
|
||||||
|
CharFilters map[string]map[string]interface{} `json:"char_filters,omitempty"`
|
||||||
|
Tokenizers map[string]map[string]interface{} `json:"tokenizers,omitempty"`
|
||||||
|
TokenMaps map[string]map[string]interface{} `json:"token_maps,omitempty"`
|
||||||
|
TokenFilters map[string]map[string]interface{} `json:"token_filters,omitempty"`
|
||||||
|
Analyzers map[string]map[string]interface{} `json:"analyzers,omitempty"`
|
||||||
|
DateTimeParsers map[string]map[string]interface{} `json:"date_time_parsers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *customAnalysis) registerAll(i *IndexMappingImpl) error {
|
||||||
|
for name, config := range c.CharFilters {
|
||||||
|
_, err := i.cache.DefineCharFilter(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Tokenizers) > 0 {
|
||||||
|
// put all the names in map tracking work to do
|
||||||
|
todo := map[string]struct{}{}
|
||||||
|
for name := range c.Tokenizers {
|
||||||
|
todo[name] = struct{}{}
|
||||||
|
}
|
||||||
|
registered := 1
|
||||||
|
errs := []error{}
|
||||||
|
// as long as we keep making progress, keep going
|
||||||
|
for len(todo) > 0 && registered > 0 {
|
||||||
|
registered = 0
|
||||||
|
errs = []error{}
|
||||||
|
for name := range todo {
|
||||||
|
config := c.Tokenizers[name]
|
||||||
|
_, err := i.cache.DefineTokenizer(name, config)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
} else {
|
||||||
|
delete(todo, name)
|
||||||
|
registered++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return errs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, config := range c.TokenMaps {
|
||||||
|
_, err := i.cache.DefineTokenMap(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, config := range c.TokenFilters {
|
||||||
|
_, err := i.cache.DefineTokenFilter(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, config := range c.Analyzers {
|
||||||
|
_, err := i.cache.DefineAnalyzer(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, config := range c.DateTimeParsers {
|
||||||
|
_, err := i.cache.DefineDateTimeParser(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCustomAnalysis() *customAnalysis {
|
||||||
|
rv := customAnalysis{
|
||||||
|
CharFilters: make(map[string]map[string]interface{}),
|
||||||
|
Tokenizers: make(map[string]map[string]interface{}),
|
||||||
|
TokenMaps: make(map[string]map[string]interface{}),
|
||||||
|
TokenFilters: make(map[string]map[string]interface{}),
|
||||||
|
Analyzers: make(map[string]map[string]interface{}),
|
||||||
|
DateTimeParsers: make(map[string]map[string]interface{}),
|
||||||
|
}
|
||||||
|
return &rv
|
||||||
|
}
|
|
@ -0,0 +1,490 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mapping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A DocumentMapping describes how a type of document
|
||||||
|
// should be indexed.
|
||||||
|
// As documents can be hierarchical, named sub-sections
|
||||||
|
// of documents are mapped using the same structure in
|
||||||
|
// the Properties field.
|
||||||
|
// Each value inside a document can be indexed 0 or more
|
||||||
|
// ways. These index entries are called fields and
|
||||||
|
// are stored in the Fields field.
|
||||||
|
// Entire sections of a document can be ignored or
|
||||||
|
// excluded by setting Enabled to false.
|
||||||
|
// If not explicitly mapped, default mapping operations
|
||||||
|
// are used. To disable this automatic handling, set
|
||||||
|
// Dynamic to false.
|
||||||
|
type DocumentMapping struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Dynamic bool `json:"dynamic"`
|
||||||
|
Properties map[string]*DocumentMapping `json:"properties,omitempty"`
|
||||||
|
Fields []*FieldMapping `json:"fields,omitempty"`
|
||||||
|
DefaultAnalyzer string `json:"default_analyzer"`
|
||||||
|
|
||||||
|
// StructTagKey overrides "json" when looking for field names in struct tags
|
||||||
|
StructTagKey string `json:"struct_tag_key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DocumentMapping) Validate(cache *registry.Cache) error {
|
||||||
|
var err error
|
||||||
|
if dm.DefaultAnalyzer != "" {
|
||||||
|
_, err := cache.AnalyzerNamed(dm.DefaultAnalyzer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, property := range dm.Properties {
|
||||||
|
err = property.Validate(cache)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, field := range dm.Fields {
|
||||||
|
if field.Analyzer != "" {
|
||||||
|
_, err = cache.AnalyzerNamed(field.Analyzer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if field.DateFormat != "" {
|
||||||
|
_, err = cache.DateTimeParserNamed(field.DateFormat)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch field.Type {
|
||||||
|
case "text", "datetime", "number", "boolean":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown field type: '%s'", field.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzerNameForPath attempts to first find the field
|
||||||
|
// described by this path, then returns the analyzer
|
||||||
|
// configured for that field
|
||||||
|
func (dm *DocumentMapping) analyzerNameForPath(path string) string {
|
||||||
|
field := dm.fieldDescribedByPath(path)
|
||||||
|
if field != nil {
|
||||||
|
return field.Analyzer
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DocumentMapping) fieldDescribedByPath(path string) *FieldMapping {
|
||||||
|
pathElements := decodePath(path)
|
||||||
|
if len(pathElements) > 1 {
|
||||||
|
// easy case, there is more than 1 path element remaining
|
||||||
|
// the next path element must match a property name
|
||||||
|
// at this level
|
||||||
|
for propName, subDocMapping := range dm.Properties {
|
||||||
|
if propName == pathElements[0] {
|
||||||
|
return subDocMapping.fieldDescribedByPath(encodePath(pathElements[1:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// just 1 path elememnt
|
||||||
|
// first look for property name with empty field
|
||||||
|
for propName, subDocMapping := range dm.Properties {
|
||||||
|
if propName == pathElements[0] {
|
||||||
|
// found property name match, now look at its fields
|
||||||
|
for _, field := range subDocMapping.Fields {
|
||||||
|
if field.Name == "" || field.Name == pathElements[0] {
|
||||||
|
// match
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// next, walk the properties again, looking for field overriding the name
|
||||||
|
for propName, subDocMapping := range dm.Properties {
|
||||||
|
if propName != pathElements[0] {
|
||||||
|
// property name isn't a match, but field name could override it
|
||||||
|
for _, field := range subDocMapping.Fields {
|
||||||
|
if field.Name == pathElements[0] {
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// documentMappingForPath only returns EXACT matches for a sub document
|
||||||
|
// or for an explicitly mapped field, if you want to find the
|
||||||
|
// closest document mapping to a field not explicitly mapped
|
||||||
|
// use closestDocMapping
|
||||||
|
func (dm *DocumentMapping) documentMappingForPath(path string) *DocumentMapping {
|
||||||
|
pathElements := decodePath(path)
|
||||||
|
current := dm
|
||||||
|
OUTER:
|
||||||
|
for i, pathElement := range pathElements {
|
||||||
|
for name, subDocMapping := range current.Properties {
|
||||||
|
if name == pathElement {
|
||||||
|
current = subDocMapping
|
||||||
|
continue OUTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no subDocMapping matches this pathElement
|
||||||
|
// only if this is the last element check for field name
|
||||||
|
if i == len(pathElements)-1 {
|
||||||
|
for _, field := range current.Fields {
|
||||||
|
if field.Name == pathElement {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
// closestDocMapping findest the most specific document mapping that matches
|
||||||
|
// part of the provided path
|
||||||
|
func (dm *DocumentMapping) closestDocMapping(path string) *DocumentMapping {
|
||||||
|
pathElements := decodePath(path)
|
||||||
|
current := dm
|
||||||
|
OUTER:
|
||||||
|
for _, pathElement := range pathElements {
|
||||||
|
for name, subDocMapping := range current.Properties {
|
||||||
|
if name == pathElement {
|
||||||
|
current = subDocMapping
|
||||||
|
continue OUTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentMapping returns a new document mapping
|
||||||
|
// with all the default values.
|
||||||
|
func NewDocumentMapping() *DocumentMapping {
|
||||||
|
return &DocumentMapping{
|
||||||
|
Enabled: true,
|
||||||
|
Dynamic: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentStaticMapping returns a new document
|
||||||
|
// mapping that will not automatically index parts
|
||||||
|
// of a document without an explicit mapping.
|
||||||
|
func NewDocumentStaticMapping() *DocumentMapping {
|
||||||
|
return &DocumentMapping{
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentDisabledMapping returns a new document
|
||||||
|
// mapping that will not perform any indexing.
|
||||||
|
func NewDocumentDisabledMapping() *DocumentMapping {
|
||||||
|
return &DocumentMapping{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSubDocumentMapping adds the provided DocumentMapping as a sub-mapping
|
||||||
|
// for the specified named subsection.
|
||||||
|
func (dm *DocumentMapping) AddSubDocumentMapping(property string, sdm *DocumentMapping) {
|
||||||
|
if dm.Properties == nil {
|
||||||
|
dm.Properties = make(map[string]*DocumentMapping)
|
||||||
|
}
|
||||||
|
dm.Properties[property] = sdm
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFieldMappingsAt adds one or more FieldMappings
|
||||||
|
// at the named sub-document. If the named sub-document
|
||||||
|
// doesn't yet exist it is created for you.
|
||||||
|
// This is a convenience function to make most common
|
||||||
|
// mappings more concise.
|
||||||
|
// Otherwise, you would:
|
||||||
|
// subMapping := NewDocumentMapping()
|
||||||
|
// subMapping.AddFieldMapping(fieldMapping)
|
||||||
|
// parentMapping.AddSubDocumentMapping(property, subMapping)
|
||||||
|
func (dm *DocumentMapping) AddFieldMappingsAt(property string, fms ...*FieldMapping) {
|
||||||
|
if dm.Properties == nil {
|
||||||
|
dm.Properties = make(map[string]*DocumentMapping)
|
||||||
|
}
|
||||||
|
sdm, ok := dm.Properties[property]
|
||||||
|
if !ok {
|
||||||
|
sdm = NewDocumentMapping()
|
||||||
|
}
|
||||||
|
for _, fm := range fms {
|
||||||
|
sdm.AddFieldMapping(fm)
|
||||||
|
}
|
||||||
|
dm.Properties[property] = sdm
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFieldMapping adds the provided FieldMapping for this section
|
||||||
|
// of the document.
|
||||||
|
func (dm *DocumentMapping) AddFieldMapping(fm *FieldMapping) {
|
||||||
|
if dm.Fields == nil {
|
||||||
|
dm.Fields = make([]*FieldMapping, 0)
|
||||||
|
}
|
||||||
|
dm.Fields = append(dm.Fields, fm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON offers custom unmarshaling with optional strict validation
|
||||||
|
func (dm *DocumentMapping) UnmarshalJSON(data []byte) error {
|
||||||
|
|
||||||
|
var tmp map[string]json.RawMessage
|
||||||
|
err := json.Unmarshal(data, &tmp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set defaults for fields which might have been omitted
|
||||||
|
dm.Enabled = true
|
||||||
|
dm.Dynamic = true
|
||||||
|
|
||||||
|
var invalidKeys []string
|
||||||
|
for k, v := range tmp {
|
||||||
|
switch k {
|
||||||
|
case "enabled":
|
||||||
|
err := json.Unmarshal(v, &dm.Enabled)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "dynamic":
|
||||||
|
err := json.Unmarshal(v, &dm.Dynamic)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "default_analyzer":
|
||||||
|
err := json.Unmarshal(v, &dm.DefaultAnalyzer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "properties":
|
||||||
|
err := json.Unmarshal(v, &dm.Properties)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "fields":
|
||||||
|
err := json.Unmarshal(v, &dm.Fields)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "struct_tag_key":
|
||||||
|
err := json.Unmarshal(v, &dm.StructTagKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
invalidKeys = append(invalidKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if MappingJSONStrict && len(invalidKeys) > 0 {
|
||||||
|
return fmt.Errorf("document mapping contains invalid keys: %v", invalidKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DocumentMapping) defaultAnalyzerName(path []string) string {
|
||||||
|
rv := ""
|
||||||
|
current := dm
|
||||||
|
for _, pathElement := range path {
|
||||||
|
var ok bool
|
||||||
|
current, ok = current.Properties[pathElement]
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if current.DefaultAnalyzer != "" {
|
||||||
|
rv = current.DefaultAnalyzer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DocumentMapping) walkDocument(data interface{}, path []string, indexes []uint64, context *walkContext) {
|
||||||
|
// allow default "json" tag to be overriden
|
||||||
|
structTagKey := dm.StructTagKey
|
||||||
|
if structTagKey == "" {
|
||||||
|
structTagKey = "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
val := reflect.ValueOf(data)
|
||||||
|
typ := val.Type()
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
// FIXME can add support for other map keys in the future
|
||||||
|
if typ.Key().Kind() == reflect.String {
|
||||||
|
for _, key := range val.MapKeys() {
|
||||||
|
fieldName := key.String()
|
||||||
|
fieldVal := val.MapIndex(key).Interface()
|
||||||
|
dm.processProperty(fieldVal, append(path, fieldName), indexes, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
fieldName := field.Name
|
||||||
|
// anonymous fields of type struct can elide the type name
|
||||||
|
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
||||||
|
fieldName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the field has a name under the specified tag, prefer that
|
||||||
|
tag := field.Tag.Get(structTagKey)
|
||||||
|
tagFieldName := parseTagName(tag)
|
||||||
|
if tagFieldName == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// allow tag to set field name to empty, only if anonymous
|
||||||
|
if field.Tag != "" && (tagFieldName != "" || field.Anonymous) {
|
||||||
|
fieldName = tagFieldName
|
||||||
|
}
|
||||||
|
|
||||||
|
if val.Field(i).CanInterface() {
|
||||||
|
fieldVal := val.Field(i).Interface()
|
||||||
|
newpath := path
|
||||||
|
if fieldName != "" {
|
||||||
|
newpath = append(path, fieldName)
|
||||||
|
}
|
||||||
|
dm.processProperty(fieldVal, newpath, indexes, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
for i := 0; i < val.Len(); i++ {
|
||||||
|
if val.Index(i).CanInterface() {
|
||||||
|
fieldVal := val.Index(i).Interface()
|
||||||
|
dm.processProperty(fieldVal, path, append(indexes, uint64(i)), context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Ptr:
|
||||||
|
ptrElem := val.Elem()
|
||||||
|
if ptrElem.IsValid() && ptrElem.CanInterface() {
|
||||||
|
dm.processProperty(ptrElem.Interface(), path, indexes, context)
|
||||||
|
}
|
||||||
|
case reflect.String:
|
||||||
|
dm.processProperty(val.String(), path, indexes, context)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
dm.processProperty(float64(val.Int()), path, indexes, context)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
dm.processProperty(float64(val.Uint()), path, indexes, context)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
dm.processProperty(float64(val.Float()), path, indexes, context)
|
||||||
|
case reflect.Bool:
|
||||||
|
dm.processProperty(val.Bool(), path, indexes, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DocumentMapping) processProperty(property interface{}, path []string, indexes []uint64, context *walkContext) {
|
||||||
|
pathString := encodePath(path)
|
||||||
|
// look to see if there is a mapping for this field
|
||||||
|
subDocMapping := dm.documentMappingForPath(pathString)
|
||||||
|
closestDocMapping := dm.closestDocMapping(pathString)
|
||||||
|
|
||||||
|
// check to see if we even need to do further processing
|
||||||
|
if subDocMapping != nil && !subDocMapping.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
propertyValue := reflect.ValueOf(property)
|
||||||
|
if !propertyValue.IsValid() {
|
||||||
|
// cannot do anything with the zero value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
propertyType := propertyValue.Type()
|
||||||
|
switch propertyType.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
propertyValueString := propertyValue.String()
|
||||||
|
if subDocMapping != nil {
|
||||||
|
// index by explicit mapping
|
||||||
|
for _, fieldMapping := range subDocMapping.Fields {
|
||||||
|
fieldMapping.processString(propertyValueString, pathString, path, indexes, context)
|
||||||
|
}
|
||||||
|
} else if closestDocMapping.Dynamic {
|
||||||
|
// automatic indexing behavior
|
||||||
|
|
||||||
|
// first see if it can be parsed by the default date parser
|
||||||
|
dateTimeParser := context.im.DateTimeParserNamed(context.im.DefaultDateTimeParser)
|
||||||
|
if dateTimeParser != nil {
|
||||||
|
parsedDateTime, err := dateTimeParser.ParseDateTime(propertyValueString)
|
||||||
|
if err != nil {
|
||||||
|
// index as text
|
||||||
|
fieldMapping := newTextFieldMappingDynamic(context.im)
|
||||||
|
fieldMapping.processString(propertyValueString, pathString, path, indexes, context)
|
||||||
|
} else {
|
||||||
|
// index as datetime
|
||||||
|
fieldMapping := newDateTimeFieldMappingDynamic(context.im)
|
||||||
|
fieldMapping.processTime(parsedDateTime, pathString, path, indexes, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
dm.processProperty(float64(propertyValue.Int()), path, indexes, context)
|
||||||
|
return
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
dm.processProperty(float64(propertyValue.Uint()), path, indexes, context)
|
||||||
|
return
|
||||||
|
case reflect.Float64, reflect.Float32:
|
||||||
|
propertyValFloat := propertyValue.Float()
|
||||||
|
if subDocMapping != nil {
|
||||||
|
// index by explicit mapping
|
||||||
|
for _, fieldMapping := range subDocMapping.Fields {
|
||||||
|
fieldMapping.processFloat64(propertyValFloat, pathString, path, indexes, context)
|
||||||
|
}
|
||||||
|
} else if closestDocMapping.Dynamic {
|
||||||
|
// automatic indexing behavior
|
||||||
|
fieldMapping := newNumericFieldMappingDynamic(context.im)
|
||||||
|
fieldMapping.processFloat64(propertyValFloat, pathString, path, indexes, context)
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
propertyValBool := propertyValue.Bool()
|
||||||
|
if subDocMapping != nil {
|
||||||
|
// index by explicit mapping
|
||||||
|
for _, fieldMapping := range subDocMapping.Fields {
|
||||||
|
fieldMapping.processBoolean(propertyValBool, pathString, path, indexes, context)
|
||||||
|
}
|
||||||
|
} else if closestDocMapping.Dynamic {
|
||||||
|
// automatic indexing behavior
|
||||||
|
fieldMapping := newBooleanFieldMappingDynamic(context.im)
|
||||||
|
fieldMapping.processBoolean(propertyValBool, pathString, path, indexes, context)
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
switch property := property.(type) {
|
||||||
|
case time.Time:
|
||||||
|
// don't descend into the time struct
|
||||||
|
if subDocMapping != nil {
|
||||||
|
// index by explicit mapping
|
||||||
|
for _, fieldMapping := range subDocMapping.Fields {
|
||||||
|
fieldMapping.processTime(property, pathString, path, indexes, context)
|
||||||
|
}
|
||||||
|
} else if closestDocMapping.Dynamic {
|
||||||
|
fieldMapping := newDateTimeFieldMappingDynamic(context.im)
|
||||||
|
fieldMapping.processTime(property, pathString, path, indexes, context)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
dm.walkDocument(property, path, indexes, context)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
dm.walkDocument(property, path, indexes, context)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,296 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mapping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
// control the default behavior for dynamic fields (those not explicitly mapped)
|
||||||
|
var (
|
||||||
|
IndexDynamic = true
|
||||||
|
StoreDynamic = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// A FieldMapping describes how a specific item
|
||||||
|
// should be put into the index.
|
||||||
|
type FieldMapping struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
|
||||||
|
// Analyzer specifies the name of the analyzer to use for this field. If
|
||||||
|
// Analyzer is empty, traverse the DocumentMapping tree toward the root and
|
||||||
|
// pick the first non-empty DefaultAnalyzer found. If there is none, use
|
||||||
|
// the IndexMapping.DefaultAnalyzer.
|
||||||
|
Analyzer string `json:"analyzer,omitempty"`
|
||||||
|
|
||||||
|
// Store indicates whether to store field values in the index. Stored
|
||||||
|
// values can be retrieved from search results using SearchRequest.Fields.
|
||||||
|
Store bool `json:"store,omitempty"`
|
||||||
|
Index bool `json:"index,omitempty"`
|
||||||
|
|
||||||
|
// IncludeTermVectors, if true, makes terms occurrences to be recorded for
|
||||||
|
// this field. It includes the term position within the terms sequence and
|
||||||
|
// the term offsets in the source document field. Term vectors are required
|
||||||
|
// to perform phrase queries or terms highlighting in source documents.
|
||||||
|
IncludeTermVectors bool `json:"include_term_vectors,omitempty"`
|
||||||
|
IncludeInAll bool `json:"include_in_all,omitempty"`
|
||||||
|
DateFormat string `json:"date_format,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextFieldMapping returns a default field mapping for text
|
||||||
|
func NewTextFieldMapping() *FieldMapping {
|
||||||
|
return &FieldMapping{
|
||||||
|
Type: "text",
|
||||||
|
Store: true,
|
||||||
|
Index: true,
|
||||||
|
IncludeTermVectors: true,
|
||||||
|
IncludeInAll: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTextFieldMappingDynamic(im *IndexMappingImpl) *FieldMapping {
|
||||||
|
rv := NewTextFieldMapping()
|
||||||
|
rv.Store = im.StoreDynamic
|
||||||
|
rv.Index = im.IndexDynamic
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNumericFieldMapping returns a default field mapping for numbers
|
||||||
|
func NewNumericFieldMapping() *FieldMapping {
|
||||||
|
return &FieldMapping{
|
||||||
|
Type: "number",
|
||||||
|
Store: true,
|
||||||
|
Index: true,
|
||||||
|
IncludeInAll: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNumericFieldMappingDynamic(im *IndexMappingImpl) *FieldMapping {
|
||||||
|
rv := NewNumericFieldMapping()
|
||||||
|
rv.Store = im.StoreDynamic
|
||||||
|
rv.Index = im.IndexDynamic
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDateTimeFieldMapping returns a default field mapping for dates
|
||||||
|
func NewDateTimeFieldMapping() *FieldMapping {
|
||||||
|
return &FieldMapping{
|
||||||
|
Type: "datetime",
|
||||||
|
Store: true,
|
||||||
|
Index: true,
|
||||||
|
IncludeInAll: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDateTimeFieldMappingDynamic(im *IndexMappingImpl) *FieldMapping {
|
||||||
|
rv := NewDateTimeFieldMapping()
|
||||||
|
rv.Store = im.StoreDynamic
|
||||||
|
rv.Index = im.IndexDynamic
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBooleanFieldMapping returns a default field mapping for booleans
|
||||||
|
func NewBooleanFieldMapping() *FieldMapping {
|
||||||
|
return &FieldMapping{
|
||||||
|
Type: "boolean",
|
||||||
|
Store: true,
|
||||||
|
Index: true,
|
||||||
|
IncludeInAll: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBooleanFieldMappingDynamic(im *IndexMappingImpl) *FieldMapping {
|
||||||
|
rv := NewBooleanFieldMapping()
|
||||||
|
rv.Store = im.StoreDynamic
|
||||||
|
rv.Index = im.IndexDynamic
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options returns the indexing options for this field.
|
||||||
|
func (fm *FieldMapping) Options() document.IndexingOptions {
|
||||||
|
var rv document.IndexingOptions
|
||||||
|
if fm.Store {
|
||||||
|
rv |= document.StoreField
|
||||||
|
}
|
||||||
|
if fm.Index {
|
||||||
|
rv |= document.IndexField
|
||||||
|
}
|
||||||
|
if fm.IncludeTermVectors {
|
||||||
|
rv |= document.IncludeTermVectors
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FieldMapping) processString(propertyValueString string, pathString string, path []string, indexes []uint64, context *walkContext) {
|
||||||
|
fieldName := getFieldName(pathString, path, fm)
|
||||||
|
options := fm.Options()
|
||||||
|
if fm.Type == "text" {
|
||||||
|
analyzer := fm.analyzerForField(path, context)
|
||||||
|
field := document.NewTextFieldCustom(fieldName, indexes, []byte(propertyValueString), options, analyzer)
|
||||||
|
context.doc.AddField(field)
|
||||||
|
|
||||||
|
if !fm.IncludeInAll {
|
||||||
|
context.excludedFromAll = append(context.excludedFromAll, fieldName)
|
||||||
|
}
|
||||||
|
} else if fm.Type == "datetime" {
|
||||||
|
dateTimeFormat := context.im.DefaultDateTimeParser
|
||||||
|
if fm.DateFormat != "" {
|
||||||
|
dateTimeFormat = fm.DateFormat
|
||||||
|
}
|
||||||
|
dateTimeParser := context.im.DateTimeParserNamed(dateTimeFormat)
|
||||||
|
if dateTimeParser != nil {
|
||||||
|
parsedDateTime, err := dateTimeParser.ParseDateTime(propertyValueString)
|
||||||
|
if err == nil {
|
||||||
|
fm.processTime(parsedDateTime, pathString, path, indexes, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FieldMapping) processFloat64(propertyValFloat float64, pathString string, path []string, indexes []uint64, context *walkContext) {
|
||||||
|
fieldName := getFieldName(pathString, path, fm)
|
||||||
|
if fm.Type == "number" {
|
||||||
|
options := fm.Options()
|
||||||
|
field := document.NewNumericFieldWithIndexingOptions(fieldName, indexes, propertyValFloat, options)
|
||||||
|
context.doc.AddField(field)
|
||||||
|
|
||||||
|
if !fm.IncludeInAll {
|
||||||
|
context.excludedFromAll = append(context.excludedFromAll, fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FieldMapping) processTime(propertyValueTime time.Time, pathString string, path []string, indexes []uint64, context *walkContext) {
|
||||||
|
fieldName := getFieldName(pathString, path, fm)
|
||||||
|
if fm.Type == "datetime" {
|
||||||
|
options := fm.Options()
|
||||||
|
field, err := document.NewDateTimeFieldWithIndexingOptions(fieldName, indexes, propertyValueTime, options)
|
||||||
|
if err == nil {
|
||||||
|
context.doc.AddField(field)
|
||||||
|
} else {
|
||||||
|
logger.Printf("could not build date %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fm.IncludeInAll {
|
||||||
|
context.excludedFromAll = append(context.excludedFromAll, fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FieldMapping) processBoolean(propertyValueBool bool, pathString string, path []string, indexes []uint64, context *walkContext) {
|
||||||
|
fieldName := getFieldName(pathString, path, fm)
|
||||||
|
if fm.Type == "boolean" {
|
||||||
|
options := fm.Options()
|
||||||
|
field := document.NewBooleanFieldWithIndexingOptions(fieldName, indexes, propertyValueBool, options)
|
||||||
|
context.doc.AddField(field)
|
||||||
|
|
||||||
|
if !fm.IncludeInAll {
|
||||||
|
context.excludedFromAll = append(context.excludedFromAll, fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FieldMapping) analyzerForField(path []string, context *walkContext) *analysis.Analyzer {
|
||||||
|
analyzerName := fm.Analyzer
|
||||||
|
if analyzerName == "" {
|
||||||
|
analyzerName = context.dm.defaultAnalyzerName(path)
|
||||||
|
if analyzerName == "" {
|
||||||
|
analyzerName = context.im.DefaultAnalyzer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context.im.AnalyzerNamed(analyzerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFieldName(pathString string, path []string, fieldMapping *FieldMapping) string {
|
||||||
|
fieldName := pathString
|
||||||
|
if fieldMapping.Name != "" {
|
||||||
|
parentName := ""
|
||||||
|
if len(path) > 1 {
|
||||||
|
parentName = encodePath(path[:len(path)-1]) + pathSeparator
|
||||||
|
}
|
||||||
|
fieldName = parentName + fieldMapping.Name
|
||||||
|
}
|
||||||
|
return fieldName
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON offers custom unmarshaling with optional strict validation
|
||||||
|
func (fm *FieldMapping) UnmarshalJSON(data []byte) error {
|
||||||
|
|
||||||
|
var tmp map[string]json.RawMessage
|
||||||
|
err := json.Unmarshal(data, &tmp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidKeys []string
|
||||||
|
for k, v := range tmp {
|
||||||
|
switch k {
|
||||||
|
case "name":
|
||||||
|
err := json.Unmarshal(v, &fm.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "type":
|
||||||
|
err := json.Unmarshal(v, &fm.Type)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "analyzer":
|
||||||
|
err := json.Unmarshal(v, &fm.Analyzer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "store":
|
||||||
|
err := json.Unmarshal(v, &fm.Store)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "index":
|
||||||
|
err := json.Unmarshal(v, &fm.Index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "include_term_vectors":
|
||||||
|
err := json.Unmarshal(v, &fm.IncludeTermVectors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "include_in_all":
|
||||||
|
err := json.Unmarshal(v, &fm.IncludeInAll)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "date_format":
|
||||||
|
err := json.Unmarshal(v, &fm.DateFormat)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
invalidKeys = append(invalidKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if MappingJSONStrict && len(invalidKeys) > 0 {
|
||||||
|
return fmt.Errorf("field mapping contains invalid keys: %v", invalidKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,430 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mapping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/analysis/analyzer/standard"
|
||||||
|
"github.com/blevesearch/bleve/analysis/datetime/optional"
|
||||||
|
"github.com/blevesearch/bleve/document"
|
||||||
|
"github.com/blevesearch/bleve/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MappingJSONStrict = false
|
||||||
|
|
||||||
|
const defaultTypeField = "_type"
|
||||||
|
const defaultType = "_default"
|
||||||
|
const defaultField = "_all"
|
||||||
|
const defaultAnalyzer = standard.Name
|
||||||
|
const defaultDateTimeParser = optional.Name
|
||||||
|
|
||||||
|
// An IndexMappingImpl controls how objects are placed
|
||||||
|
// into an index.
|
||||||
|
// First the type of the object is determined.
|
||||||
|
// Once the type is know, the appropriate
|
||||||
|
// DocumentMapping is selected by the type.
|
||||||
|
// If no mapping was determined for that type,
|
||||||
|
// a DefaultMapping will be used.
|
||||||
|
type IndexMappingImpl struct {
|
||||||
|
TypeMapping map[string]*DocumentMapping `json:"types,omitempty"`
|
||||||
|
DefaultMapping *DocumentMapping `json:"default_mapping"`
|
||||||
|
TypeField string `json:"type_field"`
|
||||||
|
DefaultType string `json:"default_type"`
|
||||||
|
DefaultAnalyzer string `json:"default_analyzer"`
|
||||||
|
DefaultDateTimeParser string `json:"default_datetime_parser"`
|
||||||
|
DefaultField string `json:"default_field"`
|
||||||
|
StoreDynamic bool `json:"store_dynamic"`
|
||||||
|
IndexDynamic bool `json:"index_dynamic"`
|
||||||
|
CustomAnalysis *customAnalysis `json:"analysis,omitempty"`
|
||||||
|
cache *registry.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCustomCharFilter defines a custom char filter for use in this mapping
|
||||||
|
func (im *IndexMappingImpl) AddCustomCharFilter(name string, config map[string]interface{}) error {
|
||||||
|
_, err := im.cache.DefineCharFilter(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
im.CustomAnalysis.CharFilters[name] = config
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCustomTokenizer defines a custom tokenizer for use in this mapping
|
||||||
|
func (im *IndexMappingImpl) AddCustomTokenizer(name string, config map[string]interface{}) error {
|
||||||
|
_, err := im.cache.DefineTokenizer(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
im.CustomAnalysis.Tokenizers[name] = config
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCustomTokenMap defines a custom token map for use in this mapping
|
||||||
|
func (im *IndexMappingImpl) AddCustomTokenMap(name string, config map[string]interface{}) error {
|
||||||
|
_, err := im.cache.DefineTokenMap(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
im.CustomAnalysis.TokenMaps[name] = config
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCustomTokenFilter defines a custom token filter for use in this mapping
|
||||||
|
func (im *IndexMappingImpl) AddCustomTokenFilter(name string, config map[string]interface{}) error {
|
||||||
|
_, err := im.cache.DefineTokenFilter(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
im.CustomAnalysis.TokenFilters[name] = config
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCustomAnalyzer defines a custom analyzer for use in this mapping. The
|
||||||
|
// config map must have a "type" string entry to resolve the analyzer
|
||||||
|
// constructor. The constructor is invoked with the remaining entries and
|
||||||
|
// returned analyzer is registered in the IndexMapping.
|
||||||
|
//
|
||||||
|
// bleve comes with predefined analyzers, like
|
||||||
|
// github.com/blevesearch/bleve/analysis/analyzers/custom_analyzer. They are
|
||||||
|
// available only if their package is imported by client code. To achieve this,
|
||||||
|
// use their metadata to fill configuration entries:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "github.com/blevesearch/bleve/analysis/analyzers/custom_analyzer"
|
||||||
|
// "github.com/blevesearch/bleve/analysis/char_filters/html_char_filter"
|
||||||
|
// "github.com/blevesearch/bleve/analysis/token_filters/lower_case_filter"
|
||||||
|
// "github.com/blevesearch/bleve/analysis/tokenizers/unicode"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// m := bleve.NewIndexMapping()
|
||||||
|
// err := m.AddCustomAnalyzer("html", map[string]interface{}{
|
||||||
|
// "type": custom_analyzer.Name,
|
||||||
|
// "char_filters": []string{
|
||||||
|
// html_char_filter.Name,
|
||||||
|
// },
|
||||||
|
// "tokenizer": unicode.Name,
|
||||||
|
// "token_filters": []string{
|
||||||
|
// lower_case_filter.Name,
|
||||||
|
// ...
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
func (im *IndexMappingImpl) AddCustomAnalyzer(name string, config map[string]interface{}) error {
|
||||||
|
_, err := im.cache.DefineAnalyzer(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
im.CustomAnalysis.Analyzers[name] = config
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCustomDateTimeParser defines a custom date time parser for use in this mapping
|
||||||
|
func (im *IndexMappingImpl) AddCustomDateTimeParser(name string, config map[string]interface{}) error {
|
||||||
|
_, err := im.cache.DefineDateTimeParser(name, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
im.CustomAnalysis.DateTimeParsers[name] = config
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIndexMapping creates a new IndexMapping that will use all the default indexing rules
|
||||||
|
func NewIndexMapping() *IndexMappingImpl {
|
||||||
|
return &IndexMappingImpl{
|
||||||
|
TypeMapping: make(map[string]*DocumentMapping),
|
||||||
|
DefaultMapping: NewDocumentMapping(),
|
||||||
|
TypeField: defaultTypeField,
|
||||||
|
DefaultType: defaultType,
|
||||||
|
DefaultAnalyzer: defaultAnalyzer,
|
||||||
|
DefaultDateTimeParser: defaultDateTimeParser,
|
||||||
|
DefaultField: defaultField,
|
||||||
|
IndexDynamic: IndexDynamic,
|
||||||
|
StoreDynamic: StoreDynamic,
|
||||||
|
CustomAnalysis: newCustomAnalysis(),
|
||||||
|
cache: registry.NewCache(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate will walk the entire structure ensuring the following
|
||||||
|
// explicitly named and default analyzers can be built
|
||||||
|
func (im *IndexMappingImpl) Validate() error {
|
||||||
|
_, err := im.cache.AnalyzerNamed(im.DefaultAnalyzer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = im.cache.DateTimeParserNamed(im.DefaultDateTimeParser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = im.DefaultMapping.Validate(im.cache)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, docMapping := range im.TypeMapping {
|
||||||
|
err = docMapping.Validate(im.cache)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDocumentMapping sets a custom document mapping for the specified type
|
||||||
|
func (im *IndexMappingImpl) AddDocumentMapping(doctype string, dm *DocumentMapping) {
|
||||||
|
im.TypeMapping[doctype] = dm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *IndexMappingImpl) mappingForType(docType string) *DocumentMapping {
|
||||||
|
docMapping := im.TypeMapping[docType]
|
||||||
|
if docMapping == nil {
|
||||||
|
docMapping = im.DefaultMapping
|
||||||
|
}
|
||||||
|
return docMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON offers custom unmarshaling with optional strict validation
|
||||||
|
func (im *IndexMappingImpl) UnmarshalJSON(data []byte) error {
|
||||||
|
|
||||||
|
var tmp map[string]json.RawMessage
|
||||||
|
err := json.Unmarshal(data, &tmp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set defaults for fields which might have been omitted
|
||||||
|
im.cache = registry.NewCache()
|
||||||
|
im.CustomAnalysis = newCustomAnalysis()
|
||||||
|
im.TypeField = defaultTypeField
|
||||||
|
im.DefaultType = defaultType
|
||||||
|
im.DefaultAnalyzer = defaultAnalyzer
|
||||||
|
im.DefaultDateTimeParser = defaultDateTimeParser
|
||||||
|
im.DefaultField = defaultField
|
||||||
|
im.DefaultMapping = NewDocumentMapping()
|
||||||
|
im.TypeMapping = make(map[string]*DocumentMapping)
|
||||||
|
im.StoreDynamic = StoreDynamic
|
||||||
|
im.IndexDynamic = IndexDynamic
|
||||||
|
|
||||||
|
var invalidKeys []string
|
||||||
|
for k, v := range tmp {
|
||||||
|
switch k {
|
||||||
|
case "analysis":
|
||||||
|
err := json.Unmarshal(v, &im.CustomAnalysis)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "type_field":
|
||||||
|
err := json.Unmarshal(v, &im.TypeField)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "default_type":
|
||||||
|
err := json.Unmarshal(v, &im.DefaultType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "default_analyzer":
|
||||||
|
err := json.Unmarshal(v, &im.DefaultAnalyzer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "default_datetime_parser":
|
||||||
|
err := json.Unmarshal(v, &im.DefaultDateTimeParser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "default_field":
|
||||||
|
err := json.Unmarshal(v, &im.DefaultField)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "default_mapping":
|
||||||
|
err := json.Unmarshal(v, &im.DefaultMapping)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "types":
|
||||||
|
err := json.Unmarshal(v, &im.TypeMapping)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "store_dynamic":
|
||||||
|
err := json.Unmarshal(v, &im.StoreDynamic)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "index_dynamic":
|
||||||
|
err := json.Unmarshal(v, &im.IndexDynamic)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
invalidKeys = append(invalidKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if MappingJSONStrict && len(invalidKeys) > 0 {
|
||||||
|
return fmt.Errorf("index mapping contains invalid keys: %v", invalidKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = im.CustomAnalysis.registerAll(im)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *IndexMappingImpl) determineType(data interface{}) string {
|
||||||
|
// first see if the object implements Classifier
|
||||||
|
classifier, ok := data.(Classifier)
|
||||||
|
if ok {
|
||||||
|
return classifier.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
// now see if we can find a type using the mapping
|
||||||
|
typ, ok := mustString(lookupPropertyPath(data, im.TypeField))
|
||||||
|
if ok {
|
||||||
|
return typ
|
||||||
|
}
|
||||||
|
|
||||||
|
return im.DefaultType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *IndexMappingImpl) MapDocument(doc *document.Document, data interface{}) error {
|
||||||
|
docType := im.determineType(data)
|
||||||
|
docMapping := im.mappingForType(docType)
|
||||||
|
walkContext := im.newWalkContext(doc, docMapping)
|
||||||
|
if docMapping.Enabled {
|
||||||
|
docMapping.walkDocument(data, []string{}, []uint64{}, walkContext)
|
||||||
|
|
||||||
|
// see if the _all field was disabled
|
||||||
|
allMapping := docMapping.documentMappingForPath("_all")
|
||||||
|
if allMapping == nil || (allMapping.Enabled != false) {
|
||||||
|
field := document.NewCompositeFieldWithIndexingOptions("_all", true, []string{}, walkContext.excludedFromAll, document.IndexField|document.IncludeTermVectors)
|
||||||
|
doc.AddField(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type walkContext struct {
|
||||||
|
doc *document.Document
|
||||||
|
im *IndexMappingImpl
|
||||||
|
dm *DocumentMapping
|
||||||
|
excludedFromAll []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *IndexMappingImpl) newWalkContext(doc *document.Document, dm *DocumentMapping) *walkContext {
|
||||||
|
return &walkContext{
|
||||||
|
doc: doc,
|
||||||
|
im: im,
|
||||||
|
dm: dm,
|
||||||
|
excludedFromAll: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnalyzerNameForPath attempts to find the best analyzer to use with only a
|
||||||
|
// field name will walk all the document types, look for field mappings at the
|
||||||
|
// provided path, if one exists and it has an explicit analyzer that is
|
||||||
|
// returned.
|
||||||
|
func (im *IndexMappingImpl) AnalyzerNameForPath(path string) string {
|
||||||
|
// first we look for explicit mapping on the field
|
||||||
|
for _, docMapping := range im.TypeMapping {
|
||||||
|
analyzerName := docMapping.analyzerNameForPath(path)
|
||||||
|
if analyzerName != "" {
|
||||||
|
return analyzerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now try the default mapping
|
||||||
|
pathMapping := im.DefaultMapping.documentMappingForPath(path)
|
||||||
|
if pathMapping != nil {
|
||||||
|
if len(pathMapping.Fields) > 0 {
|
||||||
|
if pathMapping.Fields[0].Analyzer != "" {
|
||||||
|
return pathMapping.Fields[0].Analyzer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// next we will try default analyzers for the path
|
||||||
|
pathDecoded := decodePath(path)
|
||||||
|
for _, docMapping := range im.TypeMapping {
|
||||||
|
rv := docMapping.defaultAnalyzerName(pathDecoded)
|
||||||
|
if rv != "" {
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return im.DefaultAnalyzer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *IndexMappingImpl) AnalyzerNamed(name string) *analysis.Analyzer {
|
||||||
|
analyzer, err := im.cache.AnalyzerNamed(name)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("error using analyzer named: %s", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return analyzer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *IndexMappingImpl) DateTimeParserNamed(name string) analysis.DateTimeParser {
|
||||||
|
if name == "" {
|
||||||
|
name = im.DefaultDateTimeParser
|
||||||
|
}
|
||||||
|
dateTimeParser, err := im.cache.DateTimeParserNamed(name)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("error using datetime parser named: %s", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dateTimeParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *IndexMappingImpl) datetimeParserNameForPath(path string) string {
|
||||||
|
|
||||||
|
// first we look for explicit mapping on the field
|
||||||
|
for _, docMapping := range im.TypeMapping {
|
||||||
|
pathMapping := docMapping.documentMappingForPath(path)
|
||||||
|
if pathMapping != nil {
|
||||||
|
if len(pathMapping.Fields) > 0 {
|
||||||
|
if pathMapping.Fields[0].Analyzer != "" {
|
||||||
|
return pathMapping.Fields[0].Analyzer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return im.DefaultDateTimeParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *IndexMappingImpl) AnalyzeText(analyzerName string, text []byte) (analysis.TokenStream, error) {
|
||||||
|
analyzer, err := im.cache.AnalyzerNamed(analyzerName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return analyzer.Analyze(text), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldAnalyzer returns the name of the analyzer used on a field.
|
||||||
|
func (im *IndexMappingImpl) FieldAnalyzer(field string) string {
|
||||||
|
return im.AnalyzerNameForPath(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapper to satisfy new interface
|
||||||
|
|
||||||
|
func (im *IndexMappingImpl) DefaultSearchField() string {
|
||||||
|
return im.DefaultField
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mapping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
"github.com/blevesearch/bleve/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Classifier is an interface describing any object
|
||||||
|
// which knows how to identify its own type.
|
||||||
|
type Classifier interface {
|
||||||
|
Type() string
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = log.New(ioutil.Discard, "bleve mapping ", log.LstdFlags)
|
||||||
|
|
||||||
|
// SetLog sets the logger used for logging
|
||||||
|
// by default log messages are sent to ioutil.Discard
|
||||||
|
func SetLog(l *log.Logger) {
|
||||||
|
logger = l
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexMapping interface {
|
||||||
|
MapDocument(doc *document.Document, data interface{}) error
|
||||||
|
Validate() error
|
||||||
|
|
||||||
|
DateTimeParserNamed(name string) analysis.DateTimeParser
|
||||||
|
|
||||||
|
DefaultSearchField() string
|
||||||
|
|
||||||
|
AnalyzerNameForPath(path string) string
|
||||||
|
AnalyzerNamed(name string) *analysis.Analyzer
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mapping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lookupPropertyPath(data interface{}, path string) interface{} {
|
||||||
|
pathParts := decodePath(path)
|
||||||
|
|
||||||
|
current := data
|
||||||
|
for _, part := range pathParts {
|
||||||
|
current = lookupPropertyPathPart(current, part)
|
||||||
|
if current == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupPropertyPathPart(data interface{}, part string) interface{} {
|
||||||
|
val := reflect.ValueOf(data)
|
||||||
|
typ := val.Type()
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
// FIXME can add support for other map keys in the future
|
||||||
|
if typ.Key().Kind() == reflect.String {
|
||||||
|
key := reflect.ValueOf(part)
|
||||||
|
entry := val.MapIndex(key)
|
||||||
|
if entry.IsValid() {
|
||||||
|
return entry.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
field := val.FieldByName(part)
|
||||||
|
if field.IsValid() && field.CanInterface() {
|
||||||
|
return field.Interface()
|
||||||
|
}
|
||||||
|
case reflect.Ptr:
|
||||||
|
ptrElem := val.Elem()
|
||||||
|
if ptrElem.IsValid() && ptrElem.CanInterface() {
|
||||||
|
return lookupPropertyPathPart(ptrElem.Interface(), part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathSeparator = "."
|
||||||
|
|
||||||
|
func decodePath(path string) []string {
|
||||||
|
return strings.Split(path, pathSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodePath(pathElements []string) string {
|
||||||
|
return strings.Join(pathElements, pathSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustString(data interface{}) (string, bool) {
|
||||||
|
if data != nil {
|
||||||
|
str, ok := data.(string)
|
||||||
|
if ok {
|
||||||
|
return str, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTagName extracts the field name from a struct tag
|
||||||
|
func parseTagName(tag string) string {
|
||||||
|
if idx := strings.Index(tag, ","); idx != -1 {
|
||||||
|
return tag[:idx]
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package numeric
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Float64ToInt64(f float64) int64 {
|
||||||
|
fasint := int64(math.Float64bits(f))
|
||||||
|
if fasint < 0 {
|
||||||
|
fasint = fasint ^ 0x7fffffffffffffff
|
||||||
|
}
|
||||||
|
return fasint
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int64ToFloat64(i int64) float64 {
|
||||||
|
if i < 0 {
|
||||||
|
i ^= 0x7fffffffffffffff
|
||||||
|
}
|
||||||
|
return math.Float64frombits(uint64(i))
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package numeric
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const ShiftStartInt64 byte = 0x20
|
||||||
|
|
||||||
|
// PrefixCoded is a byte array encoding of
|
||||||
|
// 64-bit numeric values shifted by 0-63 bits
|
||||||
|
type PrefixCoded []byte
|
||||||
|
|
||||||
|
func NewPrefixCodedInt64(in int64, shift uint) (PrefixCoded, error) {
|
||||||
|
if shift > 63 {
|
||||||
|
return nil, fmt.Errorf("cannot shift %d, must be between 0 and 63", shift)
|
||||||
|
}
|
||||||
|
|
||||||
|
nChars := ((63 - shift) / 7) + 1
|
||||||
|
rv := make(PrefixCoded, nChars+1)
|
||||||
|
rv[0] = ShiftStartInt64 + byte(shift)
|
||||||
|
|
||||||
|
sortableBits := int64(uint64(in) ^ 0x8000000000000000)
|
||||||
|
sortableBits = int64(uint64(sortableBits) >> shift)
|
||||||
|
for nChars > 0 {
|
||||||
|
// Store 7 bits per byte for compatibility
|
||||||
|
// with UTF-8 encoding of terms
|
||||||
|
rv[nChars] = byte(sortableBits & 0x7f)
|
||||||
|
nChars--
|
||||||
|
sortableBits = int64(uint64(sortableBits) >> 7)
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustNewPrefixCodedInt64(in int64, shift uint) PrefixCoded {
|
||||||
|
rv, err := NewPrefixCodedInt64(in, shift)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift returns the number of bits shifted
|
||||||
|
// returns 0 if in uninitialized state
|
||||||
|
func (p PrefixCoded) Shift() (uint, error) {
|
||||||
|
if len(p) > 0 {
|
||||||
|
shift := p[0] - ShiftStartInt64
|
||||||
|
if shift < 0 || shift < 63 {
|
||||||
|
return uint(shift), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("invalid prefix coded value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PrefixCoded) Int64() (int64, error) {
|
||||||
|
shift, err := p.Shift()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var sortableBits int64
|
||||||
|
for _, inbyte := range p[1:] {
|
||||||
|
sortableBits <<= 7
|
||||||
|
sortableBits |= int64(inbyte)
|
||||||
|
}
|
||||||
|
return int64(uint64((sortableBits << shift)) ^ 0x8000000000000000), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidPrefixCodedTerm(p string) (bool, int) {
|
||||||
|
if len(p) > 0 {
|
||||||
|
if p[0] < ShiftStartInt64 || p[0] > ShiftStartInt64+63 {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
shift := p[0] - ShiftStartInt64
|
||||||
|
nChars := ((63 - int(shift)) / 7) + 1
|
||||||
|
if len(p) != nChars+1 {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
return true, int(shift)
|
||||||
|
}
|
||||||
|
return false, 0
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bleve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/search/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewBoolFieldQuery creates a new Query for boolean fields
|
||||||
|
func NewBoolFieldQuery(val bool) *query.BoolFieldQuery {
|
||||||
|
return query.NewBoolFieldQuery(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBooleanQuery creates a compound Query composed
|
||||||
|
// of several other Query objects.
|
||||||
|
// These other query objects are added using the
|
||||||
|
// AddMust() AddShould() and AddMustNot() methods.
|
||||||
|
// Result documents must satisfy ALL of the
|
||||||
|
// must Queries.
|
||||||
|
// Result documents must satisfy NONE of the must not
|
||||||
|
// Queries.
|
||||||
|
// Result documents that ALSO satisfy any of the should
|
||||||
|
// Queries will score higher.
|
||||||
|
func NewBooleanQuery() *query.BooleanQuery {
|
||||||
|
return query.NewBooleanQuery(nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConjunctionQuery creates a new compound Query.
|
||||||
|
// Result documents must satisfy all of the queries.
|
||||||
|
func NewConjunctionQuery(conjuncts ...query.Query) *query.ConjunctionQuery {
|
||||||
|
return query.NewConjunctionQuery(conjuncts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDateRangeQuery creates a new Query for ranges
|
||||||
|
// of date values.
|
||||||
|
// Date strings are parsed using the DateTimeParser configured in the
|
||||||
|
// top-level config.QueryDateTimeParser
|
||||||
|
// Either, but not both endpoints can be nil.
|
||||||
|
func NewDateRangeQuery(start, end time.Time) *query.DateRangeQuery {
|
||||||
|
return query.NewDateRangeQuery(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDateRangeInclusiveQuery creates a new Query for ranges
|
||||||
|
// of date values.
|
||||||
|
// Date strings are parsed using the DateTimeParser configured in the
|
||||||
|
// top-level config.QueryDateTimeParser
|
||||||
|
// Either, but not both endpoints can be nil.
|
||||||
|
// startInclusive and endInclusive control inclusion of the endpoints.
|
||||||
|
func NewDateRangeInclusiveQuery(start, end time.Time, startInclusive, endInclusive *bool) *query.DateRangeQuery {
|
||||||
|
return query.NewDateRangeInclusiveQuery(start, end, startInclusive, endInclusive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDisjunctionQuery creates a new compound Query.
|
||||||
|
// Result documents satisfy at least one Query.
|
||||||
|
func NewDisjunctionQuery(disjuncts ...query.Query) *query.DisjunctionQuery {
|
||||||
|
return query.NewDisjunctionQuery(disjuncts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocIDQuery creates a new Query object returning indexed documents among
|
||||||
|
// the specified set. Combine it with ConjunctionQuery to restrict the scope of
|
||||||
|
// other queries output.
|
||||||
|
func NewDocIDQuery(ids []string) *query.DocIDQuery {
|
||||||
|
return query.NewDocIDQuery(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFuzzyQuery creates a new Query which finds
|
||||||
|
// documents containing terms within a specific
|
||||||
|
// fuzziness of the specified term.
|
||||||
|
// The default fuzziness is 1.
|
||||||
|
//
|
||||||
|
// The current implementation uses Levenshtein edit
|
||||||
|
// distance as the fuzziness metric.
|
||||||
|
func NewFuzzyQuery(term string) *query.FuzzyQuery {
|
||||||
|
return query.NewFuzzyQuery(term)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMatchAllQuery creates a Query which will
|
||||||
|
// match all documents in the index.
|
||||||
|
func NewMatchAllQuery() *query.MatchAllQuery {
|
||||||
|
return query.NewMatchAllQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMatchNoneQuery creates a Query which will not
|
||||||
|
// match any documents in the index.
|
||||||
|
func NewMatchNoneQuery() *query.MatchNoneQuery {
|
||||||
|
return query.NewMatchNoneQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMatchPhraseQuery creates a new Query object
|
||||||
|
// for matching phrases in the index.
|
||||||
|
// An Analyzer is chosen based on the field.
|
||||||
|
// Input text is analyzed using this analyzer.
|
||||||
|
// Token terms resulting from this analysis are
|
||||||
|
// used to build a search phrase. Result documents
|
||||||
|
// must match this phrase. Queried field must have been indexed with
|
||||||
|
// IncludeTermVectors set to true.
|
||||||
|
func NewMatchPhraseQuery(matchPhrase string) *query.MatchPhraseQuery {
|
||||||
|
return query.NewMatchPhraseQuery(matchPhrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMatchQuery creates a Query for matching text.
|
||||||
|
// An Analyzer is chosen based on the field.
|
||||||
|
// Input text is analyzed using this analyzer.
|
||||||
|
// Token terms resulting from this analysis are
|
||||||
|
// used to perform term searches. Result documents
|
||||||
|
// must satisfy at least one of these term searches.
|
||||||
|
func NewMatchQuery(match string) *query.MatchQuery {
|
||||||
|
return query.NewMatchQuery(match)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNumericRangeQuery creates a new Query for ranges
|
||||||
|
// of numeric values.
|
||||||
|
// Either, but not both endpoints can be nil.
|
||||||
|
// The minimum value is inclusive.
|
||||||
|
// The maximum value is exclusive.
|
||||||
|
func NewNumericRangeQuery(min, max *float64) *query.NumericRangeQuery {
|
||||||
|
return query.NewNumericRangeQuery(min, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNumericRangeInclusiveQuery creates a new Query for ranges
|
||||||
|
// of numeric values.
|
||||||
|
// Either, but not both endpoints can be nil.
|
||||||
|
// Control endpoint inclusion with inclusiveMin, inclusiveMax.
|
||||||
|
func NewNumericRangeInclusiveQuery(min, max *float64, minInclusive, maxInclusive *bool) *query.NumericRangeQuery {
|
||||||
|
return query.NewNumericRangeInclusiveQuery(min, max, minInclusive, maxInclusive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPhraseQuery creates a new Query for finding
|
||||||
|
// exact term phrases in the index.
|
||||||
|
// The provided terms must exist in the correct
|
||||||
|
// order, at the correct index offsets, in the
|
||||||
|
// specified field. Queried field must have been indexed with
|
||||||
|
// IncludeTermVectors set to true.
|
||||||
|
func NewPhraseQuery(terms []string, field string) *query.PhraseQuery {
|
||||||
|
return query.NewPhraseQuery(terms, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrefixQuery creates a new Query which finds
|
||||||
|
// documents containing terms that start with the
|
||||||
|
// specified prefix.
|
||||||
|
func NewPrefixQuery(prefix string) *query.PrefixQuery {
|
||||||
|
return query.NewPrefixQuery(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRegexpQuery creates a new Query which finds
|
||||||
|
// documents containing terms that match the
|
||||||
|
// specified regular expression.
|
||||||
|
func NewRegexpQuery(regexp string) *query.RegexpQuery {
|
||||||
|
return query.NewRegexpQuery(regexp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQueryStringQuery creates a new Query used for
|
||||||
|
// finding documents that satisfy a query string. The
|
||||||
|
// query string is a small query language for humans.
|
||||||
|
func NewQueryStringQuery(q string) *query.QueryStringQuery {
|
||||||
|
return query.NewQueryStringQuery(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTermQuery creates a new Query for finding an
|
||||||
|
// exact term match in the index.
|
||||||
|
func NewTermQuery(term string) *query.TermQuery {
|
||||||
|
return query.NewTermQuery(term)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWildcardQuery creates a new Query which finds
|
||||||
|
// documents containing terms that match the
|
||||||
|
// specified wildcard. In the wildcard pattern '*'
|
||||||
|
// will match any sequence of 0 or more characters,
|
||||||
|
// and '?' will match any single character.
|
||||||
|
func NewWildcardQuery(wildcard string) *query.WildcardQuery {
|
||||||
|
return query.NewWildcardQuery(wildcard)
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterAnalyzer(name string, constructor AnalyzerConstructor) {
|
||||||
|
_, exists := analyzers[name]
|
||||||
|
if exists {
|
||||||
|
panic(fmt.Errorf("attempted to register duplicate analyzer named '%s'", name))
|
||||||
|
}
|
||||||
|
analyzers[name] = constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyzerConstructor func(config map[string]interface{}, cache *Cache) (*analysis.Analyzer, error)
|
||||||
|
type AnalyzerRegistry map[string]AnalyzerConstructor
|
||||||
|
|
||||||
|
type AnalyzerCache struct {
|
||||||
|
*ConcurrentCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnalyzerCache() *AnalyzerCache {
|
||||||
|
return &AnalyzerCache{
|
||||||
|
NewConcurrentCache(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyzerBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) {
|
||||||
|
cons, registered := analyzers[name]
|
||||||
|
if !registered {
|
||||||
|
return nil, fmt.Errorf("no analyzer with name or type '%s' registered", name)
|
||||||
|
}
|
||||||
|
analyzer, err := cons(config, cache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error building analyzer: %v", err)
|
||||||
|
}
|
||||||
|
return analyzer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AnalyzerCache) AnalyzerNamed(name string, cache *Cache) (*analysis.Analyzer, error) {
|
||||||
|
item, err := c.ItemNamed(name, cache, AnalyzerBuild)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return item.(*analysis.Analyzer), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AnalyzerCache) DefineAnalyzer(name string, typ string, config map[string]interface{}, cache *Cache) (*analysis.Analyzer, error) {
|
||||||
|
item, err := c.DefineItem(name, typ, config, cache, AnalyzerBuild)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrAlreadyDefined {
|
||||||
|
return nil, fmt.Errorf("analyzer named '%s' already defined", name)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return item.(*analysis.Analyzer), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyzerTypesAndInstances() ([]string, []string) {
|
||||||
|
emptyConfig := map[string]interface{}{}
|
||||||
|
emptyCache := NewCache()
|
||||||
|
var types []string
|
||||||
|
var instances []string
|
||||||
|
for name, cons := range analyzers {
|
||||||
|
_, err := cons(emptyConfig, emptyCache)
|
||||||
|
if err == nil {
|
||||||
|
instances = append(instances, name)
|
||||||
|
} else {
|
||||||
|
types = append(types, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types, instances
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright (c) 2016 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrAlreadyDefined = fmt.Errorf("item already defined")
|
||||||
|
|
||||||
|
type CacheBuild func(name string, config map[string]interface{}, cache *Cache) (interface{}, error)
|
||||||
|
|
||||||
|
type ConcurrentCache struct {
|
||||||
|
mutex sync.RWMutex
|
||||||
|
data map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConcurrentCache() *ConcurrentCache {
|
||||||
|
return &ConcurrentCache{
|
||||||
|
data: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConcurrentCache) ItemNamed(name string, cache *Cache, build CacheBuild) (interface{}, error) {
|
||||||
|
c.mutex.RLock()
|
||||||
|
item, cached := c.data[name]
|
||||||
|
if cached {
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
// give up read lock
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
// try to build it
|
||||||
|
newItem, err := build(name, nil, cache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// acquire write lock
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
// check again because it could have been created while trading locks
|
||||||
|
item, cached = c.data[name]
|
||||||
|
if cached {
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
c.data[name] = newItem
|
||||||
|
return newItem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConcurrentCache) DefineItem(name string, typ string, config map[string]interface{}, cache *Cache, build CacheBuild) (interface{}, error) {
|
||||||
|
c.mutex.RLock()
|
||||||
|
_, cached := c.data[name]
|
||||||
|
if cached {
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
return nil, ErrAlreadyDefined
|
||||||
|
}
|
||||||
|
// give up read lock so others lookups can proceed
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
// really not there, try to build it
|
||||||
|
newItem, err := build(typ, config, cache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// now we've built it, acquire lock
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
// check again because it could have been created while trading locks
|
||||||
|
_, cached = c.data[name]
|
||||||
|
if cached {
|
||||||
|
return nil, ErrAlreadyDefined
|
||||||
|
}
|
||||||
|
c.data[name] = newItem
|
||||||
|
return newItem, nil
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright (c) 2014 Couchbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/analysis"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterCharFilter(name string, constructor CharFilterConstructor) {
|
||||||
|
_, exists := charFilters[name]
|
||||||
|
if exists {
|
||||||
|
panic(fmt.Errorf("attempted to register duplicate char filter named '%s'", name))
|
||||||
|
}
|
||||||
|
charFilters[name] = constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
type CharFilterConstructor func(config map[string]interface{}, cache *Cache) (analysis.CharFilter, error)
|
||||||
|
type CharFilterRegistry map[string]CharFilterConstructor
|
||||||
|
|
||||||
|
type CharFilterCache struct {
|
||||||
|
*ConcurrentCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCharFilterCache() *CharFilterCache {
|
||||||
|
return &CharFilterCache{
|
||||||
|
NewConcurrentCache(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CharFilterBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) {
|
||||||
|
cons, registered := charFilters[name]
|
||||||
|
if !registered {
|
||||||
|
return nil, fmt.Errorf("no char filter with name or type '%s' registered", name)
|
||||||
|
}
|
||||||
|
charFilter, err := cons(config, cache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error building char filter: %v", err)
|
||||||
|
}
|
||||||
|
return charFilter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CharFilterCache) CharFilterNamed(name string, cache *Cache) (analysis.CharFilter, error) {
|
||||||
|
item, err := c.ItemNamed(name, cache, CharFilterBuild)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return item.(analysis.CharFilter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CharFilterCache) DefineCharFilter(name string, typ string, config map[string]interface{}, cache *Cache) (analysis.CharFilter, error) {
|
||||||
|
item, err := c.DefineItem(name, typ, config, cache, CharFilterBuild)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrAlreadyDefined {
|
||||||
|
return nil, fmt.Errorf("char filter named '%s' already defined", name)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return item.(analysis.CharFilter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CharFilterTypesAndInstances() ([]string, []string) {
|
||||||
|
emptyConfig := map[string]interface{}{}
|
||||||
|
emptyCache := NewCache()
|
||||||
|
var types []string
|
||||||
|
var instances []string
|
||||||
|
for name, cons := range charFilters {
|
||||||
|
_, err := cons(emptyConfig, emptyCache)
|
||||||
|
if err == nil {
|
||||||
|
instances = append(instances, name)
|
||||||
|
} else {
|
||||||
|
types = append(types, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types, instances
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue