Skip to content

Commit

Permalink
Add SearchDocuments API (#363)
Browse files Browse the repository at this point in the history
This commit deletes sorting in id order because it would be better to
keep logic simple and apply search engines if various sorting criteria are needed.
Cursor-based pagination is not possible while returning the results sorted by key.
Instead of offset-based pagination, it returns up to given size search results.
  • Loading branch information
chacha912 authored and hackerwins committed Jul 25, 2022
1 parent 3b47b96 commit 1d3755e
Show file tree
Hide file tree
Showing 10 changed files with 816 additions and 50 deletions.
628 changes: 581 additions & 47 deletions api/admin.pb.go

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion api/admin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ service Admin {
rpc ListDocuments (ListDocumentsRequest) returns (ListDocumentsResponse) {}
rpc GetDocument (GetDocumentRequest) returns (GetDocumentResponse) {}
rpc GetSnapshotMeta (GetSnapshotMetaRequest) returns (GetSnapshotMetaResponse) {}
rpc SearchDocuments (SearchDocumentsRequest) returns (SearchDocumentsResponse) {}

rpc ListChanges (ListChangesRequest) returns (ListChangesResponse) {}
}
Expand Down Expand Up @@ -95,6 +96,17 @@ message GetSnapshotMetaResponse {
uint64 lamport = 2;
}

message SearchDocumentsRequest {
string project_name = 1;
string query = 2;
int32 page_size = 3;
}

message SearchDocumentsResponse {
int32 total_count = 1;
repeated DocumentSummary documents = 2;
}

message ListChangesRequest {
string project_name = 1;
string document_key = 2;
Expand All @@ -105,4 +117,4 @@ message ListChangesRequest {

message ListChangesResponse {
repeated Change changes = 1;
}
}
4 changes: 2 additions & 2 deletions api/types/document_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"github.com/yorkie-team/yorkie/pkg/document/key"
)

// DocumentSummary represents a summary of change.
// DocumentSummary represents a summary of document.
type DocumentSummary struct {
// ID is the unique identifier of the change.
// ID is the unique identifier of the document.
ID ID

// Key is the key of the document.
Expand Down
27 changes: 27 additions & 0 deletions api/types/search_result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* 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 types

// SearchResult is a result of search.
type SearchResult[T any] struct {
// TotalCount is the total count of the elements.
TotalCount int

// Elements is a list of elements.
Elements []T
}
32 changes: 32 additions & 0 deletions server/admin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,38 @@ func (s *Server) ListDocuments(
}, nil
}

// SearchDocuments searches documents for a specified string.
func (s *Server) SearchDocuments(
ctx context.Context,
req *api.SearchDocumentsRequest,
) (*api.SearchDocumentsResponse, error) {
project, err := projects.GetProject(ctx, s.backend, req.ProjectName)
if err != nil {
return nil, err
}

result, err := documents.SearchDocumentSummaries(
ctx,
s.backend,
project,
req.Query,
int(req.PageSize),
)
if err != nil {
return nil, err
}

pbDocuments, err := converter.ToDocumentSummaries(result.Elements)
if err != nil {
return nil, err
}

return &api.SearchDocumentsResponse{
TotalCount: int32(result.TotalCount),
Documents: pbDocuments,
}, nil
}

// ListChanges lists of changes for the given document.
func (s *Server) ListChanges(
ctx context.Context,
Expand Down
9 changes: 9 additions & 0 deletions server/backend/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ type Database interface {
createDocIfNotExist bool,
) (*DocInfo, error)

// FindDocInfoByID finds the document of the given ID.
FindDocInfoByID(
ctx context.Context,
id types.ID,
Expand Down Expand Up @@ -171,4 +172,12 @@ type Database interface {
projectID types.ID,
paging types.Paging[types.ID],
) ([]*DocInfo, error)

// FindDocInfosByQuery returns the documentInfos which match the given query.
FindDocInfosByQuery(
ctx context.Context,
projectID types.ID,
query string,
pageSize int,
) (*types.SearchResult[*DocInfo], error)
}
31 changes: 31 additions & 0 deletions server/backend/database/memory/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,37 @@ func (d *DB) FindDocInfosByPaging(
return docInfos, nil
}

// FindDocInfosByQuery returns the docInfos which match the given query.
func (d *DB) FindDocInfosByQuery(
ctx context.Context,
projectID types.ID,
query string,
pageSize int,
) (*types.SearchResult[*database.DocInfo], error) {
txn := d.db.Txn(false)
defer txn.Abort()

iterator, err := txn.Get(tblDocuments, "project_id_key_prefix", projectID.String(), query)
if err != nil {
return nil, err
}

var docInfos []*database.DocInfo
count := 0
for raw := iterator.Next(); raw != nil; raw = iterator.Next() {
if count < pageSize {
info := raw.(*database.DocInfo)
docInfos = append(docInfos, info)
}
count++
}

return &types.SearchResult[*database.DocInfo]{
TotalCount: count,
Elements: docInfos,
}, nil
}

func (d *DB) findTicketByServerSeq(
txn *memdb.Txn,
docID types.ID,
Expand Down
31 changes: 31 additions & 0 deletions server/backend/database/memory/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,37 @@ func TestDB(t *testing.T) {
assert.Equal(t, docKey, docInfo.Key)
})

t.Run("search docInfos test", func(t *testing.T) {
localDB, err := memory.New()
assert.NoError(t, err)

clientInfo, err := localDB.ActivateClient(ctx, projectID, t.Name())
assert.NoError(t, err)

docKeys := []string{
"test", "test$3", "test-search", "test$0",
"search$test", "abcde", "test abc",
"test0", "test1", "test2", "test3", "test10",
"test11", "test20", "test21", "test22", "test23"}
for _, docKey := range docKeys {
_, err := localDB.FindDocInfoByKeyAndOwner(ctx, projectID, clientInfo.ID, key.Key(docKey), true)
assert.NoError(t, err)
}

res, err := localDB.FindDocInfosByQuery(ctx, projectID, "test", 10)
assert.NoError(t, err)

var keys []key.Key
for _, info := range res.Elements {
keys = append(keys, info.Key)
}

assert.EqualValues(t, []key.Key{
"test", "test abc", "test$0", "test$3", "test-search",
"test0", "test1", "test10", "test11", "test2"}, keys)
assert.Equal(t, 15, res.TotalCount)
})

t.Run("update clientInfo after PushPull test", func(t *testing.T) {
clientInfo, err := db.ActivateClient(ctx, projectID, t.Name())
assert.NoError(t, err)
Expand Down
60 changes: 60 additions & 0 deletions server/backend/database/mongo/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package mongo
import (
"context"
"fmt"
"strings"
gotime "time"

"go.mongodb.org/mongo-driver/bson"
Expand Down Expand Up @@ -863,6 +864,45 @@ func (c *Client) FindDocInfosByPaging(
return infos, nil
}

// FindDocInfosByQuery returns the docInfos which match the given query.
func (c *Client) FindDocInfosByQuery(
ctx context.Context,
projectID types.ID,
query string,
pageSize int,
) (*types.SearchResult[*database.DocInfo], error) {
encodedProjectID, err := encodeID(projectID)
if err != nil {
return nil, err
}

cursor, err := c.collection(colDocuments).Find(ctx, bson.M{
"project_id": encodedProjectID,
"key": bson.M{"$regex": primitive.Regex{
Pattern: "^" + escapeRegexp(query),
}},
})
if err != nil {
logging.From(ctx).Error(err)
return nil, err
}

var infos []*database.DocInfo
if err := cursor.All(ctx, &infos); err != nil {
logging.From(ctx).Error(cursor.Err())
return nil, cursor.Err()
}

limit := pageSize
if limit > len(infos) {
limit = len(infos)
}
return &types.SearchResult[*database.DocInfo]{
TotalCount: len(infos),
Elements: infos[:limit],
}, nil
}

// UpdateSyncedSeq updates the syncedSeq of the given client.
func (c *Client) UpdateSyncedSeq(
ctx context.Context,
Expand Down Expand Up @@ -985,3 +1025,23 @@ func (c *Client) collection(
Database(c.config.YorkieDatabase).
Collection(name, opts...)
}

// NOTE(chacha912): escapeRegexp escapes special characters by putting a backslash in front of it.
// (https://github.com/cxr29/scrud/blob/1039f8edaf5eef522275a5a848a0fca0f53224eb/query/util.go#L31-L47)
func escape(s, a string) string {
if strings.ContainsAny(s, a) {
b := make([]rune, 0)
for _, r := range s {
if strings.ContainsRune(a, r) {
b = append(b, '\\')
}
b = append(b, r)
}
return string(b)
}
return s
}

func escapeRegexp(s string) string {
return escape(s, `\.+*?()|[]{}^$`)
}
30 changes: 30 additions & 0 deletions server/documents/documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,36 @@ func GetDocumentByServerSeq(
return doc, nil
}

// SearchDocumentSummaries returns document summaries that match the query parameters.
func SearchDocumentSummaries(
ctx context.Context,
be *backend.Backend,
project *types.Project,
query string,
pageSize int,
) (*types.SearchResult[*types.DocumentSummary], error) {
res, err := be.DB.FindDocInfosByQuery(ctx, project.ID, query, pageSize)
if err != nil {
return nil, err
}

var summaries []*types.DocumentSummary
for _, docInfo := range res.Elements {
summaries = append(summaries, &types.DocumentSummary{
ID: docInfo.ID,
Key: docInfo.Key,
CreatedAt: docInfo.CreatedAt,
AccessedAt: docInfo.AccessedAt,
UpdatedAt: docInfo.UpdatedAt,
})
}

return &types.SearchResult[*types.DocumentSummary]{
TotalCount: res.TotalCount,
Elements: summaries,
}, nil
}

// FindDocInfoByKey returns a document for the given document key.
func FindDocInfoByKey(
ctx context.Context,
Expand Down

0 comments on commit 1d3755e

Please sign in to comment.