1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 05:09:42 +02:00

Implement PostgreSQL Full Text Search++

1. Full text search supports MySQL, MariaDB, Percona and now PostgreSQL.
2. Changed SQL Variant to typed enum.
3. Changed doc.Versioned from INT to BOOL.
4. Search Reindexer now parses all documents and attachments.
5. Site meta API call returns storage provider type.
6. README prep'ed for PostgreSQL support.
7. DELETE SQL statements ignore zero rows affected.

Closes #100 !!!

Co-Authored-By: Saul S <sauls8t@users.noreply.github.com>
Co-Authored-By: McMatts <matt@documize.com>
This commit is contained in:
HarveyKandola 2018-09-28 16:33:15 +01:00
parent 97beb3f4d3
commit 8a65567169
26 changed files with 274 additions and 113 deletions

View file

@ -1,4 +1,4 @@
> We're committed to providing frequent product releases to ensure self-host customers enjoy the same product as our cloud/SaaS customers. > We provide frequent product releases ensuring self-host customers enjoy the same features as our cloud/SaaS customers.
> >
> Harvey Kandola, CEO & Founder, Documize Inc. > Harvey Kandola, CEO & Founder, Documize Inc.
@ -58,19 +58,32 @@ Space view.
## Latest version ## Latest version
[Community edition: v1.70.0](https://github.com/documize/community/releases) [Community edition: v1.71.0](https://github.com/documize/community/releases)
[Enterprise edition: v1.72.0](https://documize.com/downloads) [Enterprise edition: v1.73.0](https://documize.com/downloads)
## OS support ## OS support
Documize runs on the following: Documize can be installed and run on:
- Linux - Linux
- Windows - Windows
- macOS - macOS
# Browser support Heck, Documize will probably run just fine on a Raspberry Pi 3.
## Database support
Documize supports the following database systems:
- PostgreSQL (v9.6+)
- MySQL (v5.7.10+ and v8.0.0+)
- Percona (v5.7.16-10+)
- MariaDB (10.3.0+)
Coming soon: Microsoft SQL Server 2017 (Linux/Windows).
## Browser support
Documize supports the following (evergreen) browsers: Documize supports the following (evergreen) browsers:
@ -78,6 +91,8 @@ Documize supports the following (evergreen) browsers:
- Firefox - Firefox
- Safari - Safari
- Brave - Brave
- Vivaldi
- Opera
- MS Edge (16+) - MS Edge (16+)
## Technology stack ## Technology stack
@ -87,14 +102,6 @@ Documize is built with the following technologies:
- EmberJS (v3.1.2) - EmberJS (v3.1.2)
- Go (v1.10.3) - Go (v1.10.3)
...and supports the following databases:
- MySQL (v5.7.10+)
- Percona (v5.7.16-10+)
- MariaDB (10.3.0+)
Coming soon, PostgreSQL and Microsoft SQL Server database support.
## Authentication options ## Authentication options
Besides email/password login, you can also leverage the following options. Besides email/password login, you can also leverage the following options.

View file

@ -174,13 +174,13 @@ func runScripts(runtime *env.Runtime, tx *sqlx.Tx, scripts []Script) (err error)
} }
// executeSQL runs specified SQL commands. // executeSQL runs specified SQL commands.
func executeSQL(tx *sqlx.Tx, st env.StoreType, variant string, SQLfile []byte) error { func executeSQL(tx *sqlx.Tx, st env.StoreType, variant env.StoreType, SQLfile []byte) error {
// Turn SQL file contents into runnable SQL statements. // Turn SQL file contents into runnable SQL statements.
stmts := getStatements(SQLfile) stmts := getStatements(SQLfile)
for _, stmt := range stmts { for _, stmt := range stmts {
// MariaDB has no specific JSON column type (but has JSON queries) // MariaDB has no specific JSON column type (but has JSON queries)
if st == env.StoreTypeMySQL && variant == "mariadb" { if st == env.StoreTypeMySQL && variant == env.StoreTypeMariaDB {
stmt = strings.Replace(stmt, "` JSON", "` TEXT", -1) stmt = strings.Replace(stmt, "` JSON", "` TEXT", -1)
} }

View file

@ -287,12 +287,13 @@ CREATE TABLE dmz_search (
c_itemid varchar(16) COLLATE ucs_basic NOT NULL DEFAULT '', c_itemid varchar(16) COLLATE ucs_basic NOT NULL DEFAULT '',
c_itemtype varchar(10) COLLATE ucs_basic NOT NULL, c_itemtype varchar(10) COLLATE ucs_basic NOT NULL,
c_content text COLLATE ucs_basic, c_content text COLLATE ucs_basic,
c_token TSVECTOR,
c_created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, c_created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (id) UNIQUE (id)
); );
CREATE INDEX idx_search_1 ON dmz_search (c_orgid); CREATE INDEX idx_search_1 ON dmz_search (c_orgid);
CREATE INDEX idx_search_2 ON dmz_search (c_docid); CREATE INDEX idx_search_2 ON dmz_search (c_docid);
CREATE INDEX idx_search_3 ON dmz_search USING GIN (to_tsvector('english', c_content)); CREATE INDEX idx_search_3 ON dmz_search USING GIN(c_token);
DROP TABLE IF EXISTS dmz_section; DROP TABLE IF EXISTS dmz_section;
CREATE TABLE dmz_section ( CREATE TABLE dmz_section (

View file

@ -38,7 +38,7 @@ type StoreProvider interface {
Type() StoreType Type() StoreType
// TypeVariant returns flavor of database provider. // TypeVariant returns flavor of database provider.
TypeVariant() string TypeVariant() StoreType
// SQL driver name used to open DB connection. // SQL driver name used to open DB connection.
DriverName() string DriverName() string

View file

@ -12,6 +12,7 @@
package attachment package attachment
import ( import (
"database/sql"
"strings" "strings"
"time" "time"
@ -75,8 +76,13 @@ func (s Store) GetAttachments(ctx domain.RequestContext, docID string) (a []atta
ORDER BY c_filename`), ORDER BY c_filename`),
ctx.OrgID, docID) ctx.OrgID, docID)
if err == sql.ErrNoRows {
err = nil
a = []attachment.Attachment{}
}
if err != nil { if err != nil {
err = errors.Wrap(err, "execute select attachments") err = errors.Wrap(err, "execute select attachments")
return
} }
return return
@ -94,6 +100,11 @@ func (s Store) GetAttachmentsWithData(ctx domain.RequestContext, docID string) (
ORDER BY c_filename`), ORDER BY c_filename`),
ctx.OrgID, docID) ctx.OrgID, docID)
if err == sql.ErrNoRows {
err = nil
a = []attachment.Attachment{}
}
if err != nil { if err != nil {
err = errors.Wrap(err, "execute select attachments with data") err = errors.Wrap(err, "execute select attachments with data")
} }

View file

@ -202,7 +202,7 @@ func (s Store) RemoveDocumentCategories(ctx domain.RequestContext, documentID st
// DeleteBySpace removes all category and category associations for given space. // DeleteBySpace removes all category and category associations for given space.
func (s Store) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows int64, err error) { func (s Store) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows int64, err error) {
s1 := fmt.Sprintf("DELETE FROM categorymember WHERE c_orgid='%s' AND c_groupid='%s'", ctx.OrgID, spaceID) s1 := fmt.Sprintf("DELETE FROM dmz_category_member WHERE c_orgid='%s' AND c_spaceid='%s'", ctx.OrgID, spaceID)
s.DeleteWhere(ctx.Transaction, s1) s.DeleteWhere(ctx.Transaction, s1)
s2 := fmt.Sprintf("DELETE FROM dmz_category WHERE c_orgid='%s' AND c_spaceid='%s'", ctx.OrgID, spaceID) s2 := fmt.Sprintf("DELETE FROM dmz_category WHERE c_orgid='%s' AND c_spaceid='%s'", ctx.OrgID, spaceID)

View file

@ -245,28 +245,28 @@ func (s Store) MoveActivity(ctx domain.RequestContext, documentID, oldSpaceID, n
// Delete removes the specified document. // Delete removes the specified document.
// Remove document pages, revisions, attachments, updates the search subsystem. // Remove document pages, revisions, attachments, updates the search subsystem.
func (s Store) Delete(ctx domain.RequestContext, documentID string) (rows int64, err error) { func (s Store) Delete(ctx domain.RequestContext, documentID string) (rows int64, err error) {
rows, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_section WHERE c_docid=\"%s\" AND c_orgid=\"%s\"", documentID, ctx.OrgID)) rows, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_section WHERE c_docid='%s' AND c_orgid='%s'", documentID, ctx.OrgID))
if err != nil { if err != nil {
return return
} }
_, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_section_revision WHERE c_docid=\"%s\" AND c_orgid=\"%s\"", documentID, ctx.OrgID)) _, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_section_revision WHERE c_docid='%s' AND c_orgid='%s'", documentID, ctx.OrgID))
if err != nil { if err != nil {
return return
} }
_, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_attachment WHERE c_docid=\"%s\" AND c_orgid=\"%s\"", documentID, ctx.OrgID)) _, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_attachment WHERE c_docid='%s' AND c_orgid='%s'", documentID, ctx.OrgID))
if err != nil { if err != nil {
return return
} }
_, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_category_member WHERE c_docid=\"%s\" AND c_orgid=\"%s\"", documentID, ctx.OrgID)) _, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_category_member WHERE c_docid='%s' AND c_orgid='%s'", documentID, ctx.OrgID))
if err != nil { if err != nil {
return return
} }
_, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_vote WHERE c_docid=\"%s\" AND c_orgid=\"%s\"", documentID, ctx.OrgID)) _, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_vote WHERE c_docid='%s' AND c_orgid='%s'", documentID, ctx.OrgID))
if err != nil { if err != nil {
return return
} }
@ -277,23 +277,23 @@ func (s Store) Delete(ctx domain.RequestContext, documentID string) (rows int64,
// DeleteBySpace removes all documents for given space. // DeleteBySpace removes all documents for given space.
// Remove document pages, revisions, attachments, updates the search subsystem. // Remove document pages, revisions, attachments, updates the search subsystem.
func (s Store) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows int64, err error) { func (s Store) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows int64, err error) {
rows, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_section WHERE c_docid IN (SELECT c_refid FROM dmz_doc WHERE c_spaceid=\"%s\" AND c_orgid=\"%s\")", spaceID, ctx.OrgID)) rows, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_section WHERE c_docid IN (SELECT c_refid FROM dmz_doc WHERE c_spaceid='%s' AND c_orgid='%s')", spaceID, ctx.OrgID))
if err != nil { if err != nil {
return return
} }
_, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_section_revision WHERE c_docid IN (SELECT c_refid FROM dmz_doc WHERE c_spaceid=\"%s\" AND c_orgid=\"%s\")", spaceID, ctx.OrgID)) _, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_section_revision WHERE c_docid IN (SELECT c_refid FROM dmz_doc WHERE c_spaceid='%s' AND c_orgid='%s')", spaceID, ctx.OrgID))
if err != nil { if err != nil {
return return
} }
_, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_attachment WHERE c_docid IN (SELECT c_refid FROM dmz_doc WHERE c_spaceid=\"%s\" AND c_orgid=\"%s\")", spaceID, ctx.OrgID)) _, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_attachment WHERE c_docid IN (SELECT c_refid FROM dmz_doc WHERE c_spaceid='%s' AND c_orgid='%s')", spaceID, ctx.OrgID))
if err != nil { if err != nil {
return return
} }
_, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_vote WHERE c_docid IN (SELECT c_refid FROM dmz_doc WHERE c_spaceid=\"%s\" AND c_orgid=\"%s\")", spaceID, ctx.OrgID)) _, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_vote WHERE c_docid IN (SELECT c_refid FROM dmz_doc WHERE c_spaceid='%s' AND c_orgid='%s')", spaceID, ctx.OrgID))
if err != nil { if err != nil {
return return
} }
@ -332,7 +332,7 @@ func (s Store) GetVersions(ctx domain.RequestContext, groupID string) (v []doc.V
// Any existing vote by the user is replaced. // Any existing vote by the user is replaced.
func (s Store) Vote(ctx domain.RequestContext, refID, orgID, documentID, userID string, vote int) (err error) { func (s Store) Vote(ctx domain.RequestContext, refID, orgID, documentID, userID string, vote int) (err error) {
_, err = s.DeleteWhere(ctx.Transaction, _, err = s.DeleteWhere(ctx.Transaction,
fmt.Sprintf("DELETE FROM dmz_doc_vote WHERE c_orgid=\"%s\" AND c_docid=\"%s\" AND c_voter=\"%s\"", fmt.Sprintf("DELETE FROM dmz_doc_vote WHERE c_orgid='%s' AND c_docid='%s' AND c_voter='%s'",
orgID, documentID, userID)) orgID, documentID, userID))
if err != nil { if err != nil {
s.Runtime.Log.Error("store.Vote", err) s.Runtime.Log.Error("store.Vote", err)

View file

@ -104,7 +104,7 @@ func (s Store) Delete(ctx domain.RequestContext, refID string) (rows int64, err
if err != nil { if err != nil {
return return
} }
return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_group_member WHERE c_orgid=\"%s\" AND c_groupid=\"%s\"", ctx.OrgID, refID)) return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_group_member WHERE c_orgid='%s' AND c_groupid='%s'", ctx.OrgID, refID))
} }
// GetGroupMembers returns all user associated with given group. // GetGroupMembers returns all user associated with given group.
@ -143,8 +143,12 @@ func (s Store) JoinGroup(ctx domain.RequestContext, groupID, userID string) (err
// LeaveGroup removes user from group. // LeaveGroup removes user from group.
func (s Store) LeaveGroup(ctx domain.RequestContext, groupID, userID string) (err error) { func (s Store) LeaveGroup(ctx domain.RequestContext, groupID, userID string) (err error) {
_, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_group_member WHERE c_orgid=\"%s\" AND c_groupid=\"%s\" AND c_userid=\"%s\"", _, err = s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_group_member WHERE c_orgid='%s' AND c_groupid='%s' AND c_userid='%s'",
ctx.OrgID, groupID, userID)) ctx.OrgID, groupID, userID))
if err == sql.ErrNoRows {
err = nil
}
if err != nil { if err != nil {
err = errors.Wrap(err, "clear group member") err = errors.Wrap(err, "clear group member")
} }

View file

@ -137,12 +137,12 @@ func (s Store) MarkOrphanAttachmentLink(ctx domain.RequestContext, attachmentID
// DeleteSourcePageLinks removes saved links for given source. // DeleteSourcePageLinks removes saved links for given source.
func (s Store) DeleteSourcePageLinks(ctx domain.RequestContext, pageID string) (rows int64, err error) { func (s Store) DeleteSourcePageLinks(ctx domain.RequestContext, pageID string) (rows int64, err error) {
return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_link WHERE c_orgid=\"%s\" AND c_sourcesectionid=\"%s\"", ctx.OrgID, pageID)) return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_link WHERE c_orgid='%s' AND c_sourcesectionid='%s'", ctx.OrgID, pageID))
} }
// DeleteSourceDocumentLinks removes saved links for given document. // DeleteSourceDocumentLinks removes saved links for given document.
func (s Store) DeleteSourceDocumentLinks(ctx domain.RequestContext, documentID string) (rows int64, err error) { func (s Store) DeleteSourceDocumentLinks(ctx domain.RequestContext, documentID string) (rows int64, err error) {
return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_link WHERE c_orgid=\"%s\" AND c_sourcedocid=\"%s\"", ctx.OrgID, documentID)) return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_doc_link WHERE c_orgid='%s' AND c_sourcedocid='%s'", ctx.OrgID, documentID))
} }
// DeleteLink removes saved link from the store. // DeleteLink removes saved link from the store.

View file

@ -62,6 +62,7 @@ func (h *Handler) Meta(w http.ResponseWriter, r *http.Request) {
data.Valid = h.Runtime.Product.License.Valid data.Valid = h.Runtime.Product.License.Valid
data.ConversionEndpoint = org.ConversionEndpoint data.ConversionEndpoint = org.ConversionEndpoint
data.License = h.Runtime.Product.License data.License = h.Runtime.Product.License
data.Storage = h.Runtime.StoreProvider.Type()
// Strip secrets // Strip secrets
data.AuthConfig = auth.StripAuthSecrets(h.Runtime, org.AuthProvider, org.AuthConfig) data.AuthConfig = auth.StripAuthSecrets(h.Runtime, org.AuthProvider, org.AuthConfig)
@ -213,6 +214,19 @@ func (h *Handler) rebuildSearchIndex(ctx domain.RequestContext) {
for i := range docs { for i := range docs {
d := docs[i] d := docs[i]
doc, err := h.Store.Document.Get(ctx, d)
if err != nil {
h.Runtime.Log.Error(method, err)
return
}
at, err := h.Store.Attachment.GetAttachments(ctx, d)
if err != nil {
h.Runtime.Log.Error(method, err)
return
}
h.Indexer.IndexDocument(ctx, doc, at)
pages, err := h.Store.Meta.GetDocumentPages(ctx, d) pages, err := h.Store.Meta.GetDocumentPages(ctx, d)
if err != nil { if err != nil {
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)

View file

@ -54,6 +54,10 @@ func (s Store) GetDocumentPages(ctx domain.RequestContext, documentID string) (p
WHERE c_docid=? AND (c_status=0 OR ((c_status=4 OR c_status=2) AND c_relativeid=''))`), WHERE c_docid=? AND (c_status=0 OR ((c_status=4 OR c_status=2) AND c_relativeid=''))`),
documentID) documentID)
if err == sql.ErrNoRows {
err = nil
p = []page.Page{}
}
if err != nil { if err != nil {
err = errors.Wrap(err, "failed to get instance document pages") err = errors.Wrap(err, "failed to get instance document pages")
} }

View file

@ -33,7 +33,8 @@ type Store struct {
func (s Store) AddPermission(ctx domain.RequestContext, r permission.Permission) (err error) { func (s Store) AddPermission(ctx domain.RequestContext, r permission.Permission) (err error) {
r.Created = time.Now().UTC() r.Created = time.Now().UTC()
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_permission (c_orgid, c_who, c_whoid, c_action, c_scope, c_location, c_refid, c_created) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"), _, err = ctx.Transaction.Exec(s.Bind(`INSERT INTO dmz_permission
(c_orgid, c_who, c_whoid, c_action, c_scope, c_location, c_refid, c_created) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`),
r.OrgID, string(r.Who), r.WhoID, string(r.Action), string(r.Scope), string(r.Location), r.RefID, r.Created) r.OrgID, string(r.Who), r.WhoID, string(r.Action), string(r.Scope), string(r.Location), r.RefID, r.Created)
if err != nil { if err != nil {
@ -278,7 +279,7 @@ func (s Store) DeleteSpacePermissions(ctx domain.RequestContext, spaceID string)
// DeleteUserSpacePermissions removes all roles for the specified user, for the specified space. // DeleteUserSpacePermissions removes all roles for the specified user, for the specified space.
func (s Store) DeleteUserSpacePermissions(ctx domain.RequestContext, spaceID, userID string) (rows int64, err error) { func (s Store) DeleteUserSpacePermissions(ctx domain.RequestContext, spaceID, userID string) (rows int64, err error) {
sql := fmt.Sprintf("DELETE FROM dmz_permission WHERE c_orgid='%s' AND c_location='space' AND c_refid='%s' c_who='user' AND c_whoid='%s'", sql := fmt.Sprintf("DELETE FROM dmz_permission WHERE c_orgid='%s' AND c_location='space' AND c_refid='%s' AND c_who='user' AND c_whoid='%s'",
ctx.OrgID, spaceID, userID) ctx.OrgID, spaceID, userID)
return s.DeleteWhere(ctx.Transaction, sql) return s.DeleteWhere(ctx.Transaction, sql)

View file

@ -120,10 +120,10 @@ func (s Store) DeletePin(ctx domain.RequestContext, id string) (rows int64, err
// DeletePinnedSpace removes any pins for specified space. // DeletePinnedSpace removes any pins for specified space.
func (s Store) DeletePinnedSpace(ctx domain.RequestContext, spaceID string) (rows int64, err error) { func (s Store) DeletePinnedSpace(ctx domain.RequestContext, spaceID string) (rows int64, err error) {
return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_pin WHERE c_orgid=\"%s\" AND c_spaceid=\"%s\"", ctx.OrgID, spaceID)) return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_pin WHERE c_orgid='%s' AND c_spaceid='%s'", ctx.OrgID, spaceID))
} }
// DeletePinnedDocument removes any pins for specified document. // DeletePinnedDocument removes any pins for specified document.
func (s Store) DeletePinnedDocument(ctx domain.RequestContext, documentID string) (rows int64, err error) { func (s Store) DeletePinnedDocument(ctx domain.RequestContext, documentID string) (rows int64, err error) {
return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_pin WHERE c_orgid=\"%s\" AND c_docid=\"%s\"", ctx.OrgID, documentID)) return s.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM dmz_pin WHERE c_orgid='%s' AND c_docid='%s'", ctx.OrgID, documentID))
} }

View file

@ -82,7 +82,12 @@ func (m *Indexer) IndexContent(ctx domain.RequestContext, p page.Page) {
return return
} }
ctx.Transaction.Commit() err = ctx.Transaction.Commit()
if err != nil {
ctx.Transaction.Rollback()
m.runtime.Log.Error(method, err)
return
}
} }
// DeleteContent removes all search entries for specific document content. // DeleteContent removes all search entries for specific document content.

View file

@ -39,43 +39,68 @@ type Store struct {
// IndexDocument adds search index entries for document inserting title, tags and attachments as // IndexDocument adds search index entries for document inserting title, tags and attachments as
// searchable items. Any existing document entries are removed. // searchable items. Any existing document entries are removed.
func (s Store) IndexDocument(ctx domain.RequestContext, doc doc.Document, a []attachment.Attachment) (err error) { func (s Store) IndexDocument(ctx domain.RequestContext, doc doc.Document, a []attachment.Attachment) (err error) {
method := "search.IndexDocument"
// remove previous search entries // remove previous search entries
_, err = ctx.Transaction.Exec(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_docid=? AND (c_itemtype='doc' OR c_itemtype='file' OR c_itemtype='tag')"), _, err = ctx.Transaction.Exec(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_docid=? AND (c_itemtype='doc' OR c_itemtype='file' OR c_itemtype='tag')"),
ctx.OrgID, doc.RefID) ctx.OrgID, doc.RefID)
if err != nil && err != sql.ErrNoRows {
if err != nil {
err = errors.Wrap(err, "execute delete document index entries") err = errors.Wrap(err, "execute delete document index entries")
s.Runtime.Log.Error(method, err)
return
} }
// insert doc title // insert doc title
if s.Runtime.StoreProvider.Type() == env.StoreTypePostgreSQL {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content, c_token) VALUES (?, ?, ?, ?, ?, to_tsvector(?))"),
ctx.OrgID, doc.RefID, "", "doc", doc.Name, doc.Name)
} else {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"), _, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"),
ctx.OrgID, doc.RefID, "", "doc", doc.Name) ctx.OrgID, doc.RefID, "", "doc", doc.Name)
if err != nil { }
if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "execute insert document title entry") err = errors.Wrap(err, "execute insert document title entry")
s.Runtime.Log.Error(method, err)
return
} }
// insert doc tags // insert doc tags
tags := strings.Split(doc.Tags, "#") tags := strings.Split(doc.Tags, "#")
for _, t := range tags { for _, t := range tags {
t = strings.TrimSpace(t)
if len(t) == 0 { if len(t) == 0 {
continue continue
} }
if s.Runtime.StoreProvider.Type() == env.StoreTypePostgreSQL {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content, c_token) VALUES (?, ?, ?, ?, ?, to_tsvector(?))"),
ctx.OrgID, doc.RefID, "", "tag", t, t)
} else {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"), _, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"),
ctx.OrgID, doc.RefID, "", "tag", t) ctx.OrgID, doc.RefID, "", "tag", t)
}
if err != nil { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "execute insert document tag entry") err = errors.Wrap(err, "execute insert document tag entry")
s.Runtime.Log.Error(method, err)
return return
} }
} }
for _, file := range a { for _, file := range a {
if s.Runtime.StoreProvider.Type() == env.StoreTypePostgreSQL {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content, c_token) VALUES (?, ?, ?, ?, ?, to_tsvector(?))"),
ctx.OrgID, doc.RefID, file.RefID, "file", file.Filename, file.Filename)
} else {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"), _, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"),
ctx.OrgID, doc.RefID, file.RefID, "file", file.Filename) ctx.OrgID, doc.RefID, file.RefID, "file", file.Filename)
}
if err != nil { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "execute insert document file entry") err = errors.Wrap(err, "execute insert document file entry")
s.Runtime.Log.Error(method, err)
return
} }
} }
@ -84,11 +109,14 @@ func (s Store) IndexDocument(ctx domain.RequestContext, doc doc.Document, a []at
// DeleteDocument removes all search entries for document. // DeleteDocument removes all search entries for document.
func (s Store) DeleteDocument(ctx domain.RequestContext, ID string) (err error) { func (s Store) DeleteDocument(ctx domain.RequestContext, ID string) (err error) {
method := "search.DeleteDocument"
_, err = ctx.Transaction.Exec(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_docid=?"), _, err = ctx.Transaction.Exec(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_docid=?"),
ctx.OrgID, ID) ctx.OrgID, ID)
if err != nil { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "execute delete document entries") err = errors.Wrap(err, "execute delete document entries")
s.Runtime.Log.Error(method, err)
} }
return return
@ -97,6 +125,8 @@ func (s Store) DeleteDocument(ctx domain.RequestContext, ID string) (err error)
// IndexContent adds search index entry for document context. // IndexContent adds search index entry for document context.
// Any existing document entries are removed. // Any existing document entries are removed.
func (s Store) IndexContent(ctx domain.RequestContext, p page.Page) (err error) { func (s Store) IndexContent(ctx domain.RequestContext, p page.Page) (err error) {
method := "search.IndexContent"
// we do not index pending pages // we do not index pending pages
if p.Status == workflow.ChangePending || p.Status == workflow.ChangePendingNew { if p.Status == workflow.ChangePending || p.Status == workflow.ChangePendingNew {
return return
@ -106,28 +136,49 @@ func (s Store) IndexContent(ctx domain.RequestContext, p page.Page) (err error)
_, err = ctx.Transaction.Exec(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_docid=? AND c_itemid=? AND c_itemtype='page'"), _, err = ctx.Transaction.Exec(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_docid=? AND c_itemid=? AND c_itemtype='page'"),
ctx.OrgID, p.DocumentID, p.RefID) ctx.OrgID, p.DocumentID, p.RefID)
if err != nil { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "execute delete document content entry") err = errors.Wrap(err, "execute delete document content entry")
s.Runtime.Log.Error(method, err)
return
} }
err = nil
// prepare content // prepare content
content, err := stringutil.HTML(p.Body).Text(false) content, err := stringutil.HTML(p.Body).Text(false)
if err != nil { if err != nil {
err = errors.Wrap(err, "search strip HTML failed") err = errors.Wrap(err, "search strip HTML failed")
s.Runtime.Log.Error(method, err)
return return
} }
content = strings.TrimSpace(content) content = strings.TrimSpace(content)
if s.Runtime.StoreProvider.Type() == env.StoreTypePostgreSQL {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content, c_token) VALUES (?, ?, ?, ?, ?, to_tsvector(?))"),
ctx.OrgID, p.DocumentID, p.RefID, "page", content, content)
} else {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"), _, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"),
ctx.OrgID, p.DocumentID, p.RefID, "page", content) ctx.OrgID, p.DocumentID, p.RefID, "page", content)
if err != nil {
err = errors.Wrap(err, "execute insert document content entry")
} }
if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "execute insert section content entry")
s.Runtime.Log.Error(method, err)
return
}
err = nil
if s.Runtime.StoreProvider.Type() == env.StoreTypePostgreSQL {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content, c_token) VALUES (?, ?, ?, ?, ?, to_tsvector(?))"),
ctx.OrgID, p.DocumentID, p.RefID, "page", p.Name, p.Name)
} else {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"), _, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_search (c_orgid, c_docid, c_itemid, c_itemtype, c_content) VALUES (?, ?, ?, ?, ?)"),
ctx.OrgID, p.DocumentID, p.RefID, "page", p.Name) ctx.OrgID, p.DocumentID, p.RefID, "page", p.Name)
if err != nil { }
err = errors.Wrap(err, "execute insert document page title entry") if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "execute insert section title entry")
s.Runtime.Log.Error(method, err)
return
} }
return nil return nil
@ -135,18 +186,23 @@ func (s Store) IndexContent(ctx domain.RequestContext, p page.Page) (err error)
// DeleteContent removes all search entries for specific document content. // DeleteContent removes all search entries for specific document content.
func (s Store) DeleteContent(ctx domain.RequestContext, pageID string) (err error) { func (s Store) DeleteContent(ctx domain.RequestContext, pageID string) (err error) {
method := "search.DeleteContent"
// remove all search entries // remove all search entries
var stmt1 *sqlx.Stmt var stmt1 *sqlx.Stmt
stmt1, err = ctx.Transaction.Preparex(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_itemid=? AND c_itemtype=?")) stmt1, err = ctx.Transaction.Preparex(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_itemid=? AND c_itemtype=?"))
defer streamutil.Close(stmt1) defer streamutil.Close(stmt1)
if err != nil {
if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "prepare delete document content entry") err = errors.Wrap(err, "prepare delete document content entry")
s.Runtime.Log.Error(method, err)
return return
} }
_, err = stmt1.Exec(ctx.OrgID, pageID, "page") _, err = stmt1.Exec(ctx.OrgID, pageID, "page")
if err != nil { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "execute delete document content entry") err = errors.Wrap(err, "execute delete document content entry")
s.Runtime.Log.Error(method, err)
return return
} }
@ -220,9 +276,17 @@ func (s Store) matchFullText(ctx domain.RequestContext, keywords, itemType strin
switch s.Runtime.StoreProvider.Type() { switch s.Runtime.StoreProvider.Type() {
case env.StoreTypeMySQL: case env.StoreTypeMySQL:
fts = " AND MATCH(s.c_content) AGAINST(? IN BOOLEAN MODE)" fts = " AND MATCH(s.c_content) AGAINST(? IN BOOLEAN MODE) "
case env.StoreTypePostgreSQL: case env.StoreTypePostgreSQL:
fts = "" // By default, we expect no Postgres full text search operators.
parser := "plainto_tsquery"
// If we find operators then we have to use correct query processor.
operator := strings.ContainsAny(keywords, "!()&|*'`\":<->")
if operator {
parser = "to_tsquery"
}
fts = fmt.Sprintf(" AND s.c_token @@ %s(?) ", parser)
} }
sql1 := s.Bind(` sql1 := s.Bind(`
@ -279,7 +343,7 @@ func (s Store) matchLike(ctx domain.RequestContext, keywords, itemType string) (
keywords = strings.Replace(keywords, "'", "", -1) keywords = strings.Replace(keywords, "'", "", -1)
keywords = strings.Replace(keywords, "\"", "", -1) keywords = strings.Replace(keywords, "\"", "", -1)
keywords = strings.Replace(keywords, "%", "", -1) keywords = strings.Replace(keywords, "%", "", -1)
keywords = fmt.Sprintf("%%%s%%", keywords) keywords = fmt.Sprintf("%%%s%%", strings.ToLower(keywords))
sql1 := s.Bind(`SELECT sql1 := s.Bind(`SELECT
s.id, s.c_orgid AS orgid, s.c_docid AS documentid, s.c_itemid AS itemid, s.c_itemtype AS itemtype, s.id, s.c_orgid AS orgid, s.c_docid AS documentid, s.c_itemid AS itemid, s.c_itemtype AS itemtype,
@ -304,7 +368,7 @@ func (s Store) matchLike(ctx domain.RequestContext, keywords, itemType string) (
AND p.c_location='space' AND (r.c_userid=? OR r.c_userid='0') AND p.c_location='space' AND (r.c_userid=? OR r.c_userid='0')
) )
) )
AND s.c_content LIKE ?`) AND LOWER(s.c_content) LIKE ?`)
err = s.Runtime.Db.Select(&r, err = s.Runtime.Db.Select(&r,
sql1, sql1,

View file

@ -470,7 +470,13 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
// If newly marked Everyone space, ensure everyone has permission // If newly marked Everyone space, ensure everyone has permission
if prev.Type != space.ScopePublic && sp.Type == space.ScopePublic { if prev.Type != space.ScopePublic && sp.Type == space.ScopePublic {
h.Store.Permission.DeleteUserSpacePermissions(ctx, sp.RefID, user.EveryoneUserID) _, err = h.Store.Permission.DeleteUserSpacePermissions(ctx, sp.RefID, user.EveryoneUserID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
perm := permission.Permission{} perm := permission.Permission{}
perm.OrgID = sp.OrgID perm.OrgID = sp.OrgID

View file

@ -12,6 +12,7 @@
package store package store
import ( import (
"database/sql"
"fmt" "fmt"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -44,12 +45,13 @@ func (c *Context) Bind(sql string) string {
func (c *Context) Delete(tx *sqlx.Tx, table string, id string) (rows int64, err error) { func (c *Context) Delete(tx *sqlx.Tx, table string, id string) (rows int64, err error) {
result, err := tx.Exec(c.Bind("DELETE FROM "+table+" WHERE c_refid=?"), id) result, err := tx.Exec(c.Bind("DELETE FROM "+table+" WHERE c_refid=?"), id)
if err != nil { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table)) err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table))
return return
} }
rows, err = result.RowsAffected() rows, err = result.RowsAffected()
err = nil
return return
} }
@ -58,12 +60,13 @@ func (c *Context) Delete(tx *sqlx.Tx, table string, id string) (rows int64, err
func (c *Context) DeleteConstrained(tx *sqlx.Tx, table string, orgID, id string) (rows int64, err error) { func (c *Context) DeleteConstrained(tx *sqlx.Tx, table string, orgID, id string) (rows int64, err error) {
result, err := tx.Exec(c.Bind("DELETE FROM "+table+" WHERE c_orgid=? AND c_refid=?"), orgID, id) result, err := tx.Exec(c.Bind("DELETE FROM "+table+" WHERE c_orgid=? AND c_refid=?"), orgID, id)
if err != nil { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table)) err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table))
return return
} }
rows, err = result.RowsAffected() rows, err = result.RowsAffected()
err = nil
return return
} }
@ -72,12 +75,13 @@ func (c *Context) DeleteConstrained(tx *sqlx.Tx, table string, orgID, id string)
func (c *Context) DeleteConstrainedWithID(tx *sqlx.Tx, table string, orgID, id string) (rows int64, err error) { func (c *Context) DeleteConstrainedWithID(tx *sqlx.Tx, table string, orgID, id string) (rows int64, err error) {
result, err := tx.Exec(c.Bind("DELETE FROM "+table+" WHERE c_orgid=? AND id=?"), orgID, id) result, err := tx.Exec(c.Bind("DELETE FROM "+table+" WHERE c_orgid=? AND id=?"), orgID, id)
if err != nil { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table)) err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table))
return return
} }
rows, err = result.RowsAffected() rows, err = result.RowsAffected()
err = nil
return return
} }
@ -86,12 +90,13 @@ func (c *Context) DeleteConstrainedWithID(tx *sqlx.Tx, table string, orgID, id s
func (c *Context) DeleteWhere(tx *sqlx.Tx, statement string) (rows int64, err error) { func (c *Context) DeleteWhere(tx *sqlx.Tx, statement string) (rows int64, err error) {
result, err := tx.Exec(statement) result, err := tx.Exec(statement)
if err != nil { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, fmt.Sprintf("unable to delete rows: %s", statement)) err = errors.Wrap(err, fmt.Sprintf("unable to delete rows: %s", statement))
return return
} }
rows, err = result.RowsAffected() rows, err = result.RowsAffected()
err = nil
return return
} }

View file

@ -43,13 +43,24 @@ import (
// SetMySQLProvider creates MySQL provider // SetMySQLProvider creates MySQL provider
func SetMySQLProvider(r *env.Runtime, s *store.Store) { func SetMySQLProvider(r *env.Runtime, s *store.Store) {
// Set up provider specific details and wire up data prividers. // Set up provider specific details.
r.StoreProvider = MySQLProvider{ p := MySQLProvider{
ConnectionString: r.Flags.DBConn, ConnectionString: r.Flags.DBConn,
Variant: r.Flags.DBType, }
switch r.Flags.DBType {
case "mysql":
p.Variant = env.StoreTypeMySQL
case "mariadb":
p.Variant = env.StoreTypeMariaDB
case "percona":
p.Variant = env.StoreTypePercona
} }
// Wire up data providers! r.StoreProvider = p
// Wire up data providers.
// Account
accountStore := account.Store{} accountStore := account.Store{}
accountStore.Runtime = r accountStore.Runtime = r
s.Account = accountStore s.Account = accountStore
@ -146,7 +157,7 @@ type MySQLProvider struct {
ConnectionString string ConnectionString string
// User specified db type (mysql, percona or mariadb). // User specified db type (mysql, percona or mariadb).
Variant string Variant env.StoreType
} }
// Type returns name of provider // Type returns name of provider
@ -155,7 +166,7 @@ func (p MySQLProvider) Type() env.StoreType {
} }
// TypeVariant returns databse flavor // TypeVariant returns databse flavor
func (p MySQLProvider) TypeVariant() string { func (p MySQLProvider) TypeVariant() env.StoreType {
return p.Variant return p.Variant
} }

View file

@ -45,18 +45,20 @@ type PostgreSQLProvider struct {
ConnectionString string ConnectionString string
// Unused for this provider. // Unused for this provider.
Variant string Variant env.StoreType
} }
// SetPostgreSQLProvider creates PostgreSQL provider // SetPostgreSQLProvider creates PostgreSQL provider
func SetPostgreSQLProvider(r *env.Runtime, s *store.Store) { func SetPostgreSQLProvider(r *env.Runtime, s *store.Store) {
// Set up provider specific details and wire up data prividers. // Set up provider specific details.
r.StoreProvider = PostgreSQLProvider{ r.StoreProvider = PostgreSQLProvider{
ConnectionString: r.Flags.DBConn, ConnectionString: r.Flags.DBConn,
Variant: "", Variant: env.StoreTypePostgreSQL,
} }
// Wire up data providers! // Wire up data providers.
// Account
accountStore := account.Store{} accountStore := account.Store{}
accountStore.Runtime = r accountStore.Runtime = r
s.Account = accountStore s.Account = accountStore
@ -153,7 +155,7 @@ func (p PostgreSQLProvider) Type() env.StoreType {
} }
// TypeVariant returns databse flavor // TypeVariant returns databse flavor
func (p PostgreSQLProvider) TypeVariant() string { func (p PostgreSQLProvider) TypeVariant() env.StoreType {
return p.Variant return p.Variant
} }

View file

@ -135,6 +135,12 @@ let constants = EmberObject.extend({
Rejected: 6, Rejected: 6,
Publish: 7, Publish: 7,
}, },
// Meta
StoreProvider: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
MySQL: 'MySQL',
PostgreSQL: 'PostgreSQL',
},
}); });
export default { constants } export default { constants }

View file

@ -29,6 +29,6 @@ export default Route.extend(AuthenticatedRouteMixin, {
}, },
activate() { activate() {
this.get('browser').setTitle('Search'); this.get('browser').setTitle('Search Engine');
} }
}); });

View file

@ -9,9 +9,12 @@
// //
// https://documize.com // https://documize.com
import { inject as service } from '@ember/service';
import Controller from '@ember/controller'; import Controller from '@ember/controller';
export default Controller.extend({ export default Controller.extend({
appMeta: service(),
queryParams: ['filter', 'matchDoc', 'matchContent', 'matchTag', 'matchFile', 'slog'], queryParams: ['filter', 'matchDoc', 'matchContent', 'matchTag', 'matchFile', 'slog'],
filter: '', filter: '',
matchDoc: true, matchDoc: true,

View file

@ -9,8 +9,8 @@
// //
// https://documize.com // https://documize.com
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import Route from '@ember/routing/route';
export default Route.extend(AuthenticatedRouteMixin, { export default Route.extend(AuthenticatedRouteMixin, {
activate() { activate() {

View file

@ -10,22 +10,36 @@
<div id="sidebar" class="sidebar"> <div id="sidebar" class="sidebar">
<h1>Search</h1> <h1>Search</h1>
<div class="view-search"> <div class="view-search">
{{#if (eq appMeta.storageProvider constants.StoreProvider.MySQL)}}
<div class="syntax"> <div class="syntax">
<div class="example">apple banana</div> <div class="example">apple banana</div>
<div class="explain">Find rows that contain at least one of the two words</div> <div class="explain">Show results that contain at least one of the two words</div>
<div class="example">+apple +banana</div> <div class="example">+apple +banana</div>
<div class="explain">Find rows that contain both words</div> <div class="explain">Show results that contain both words</div>
<div class="example">+apple macintosh</div> <div class="example">+apple macintosh</div>
<div class="explain">Find rows that contain the word "apple", but rank rows higher if they also contain "macintosh"</div> <div class="explain">Show results that contain the word "apple", but rank rows higher if they also contain "macintosh"</div>
<div class="example">+apple -macintosh</div> <div class="example">+apple -macintosh</div>
<div class="explain">Find rows that contain the word "apple" but not "macintosh"</div> <div class="explain">Show results that contain the word "apple" but not "macintosh"</div>
<div class="example">+apple +(&gt;turnover &lt;strudel)</div> <div class="example">+apple +(&gt;turnover &lt;strudel)</div>
<div class="explain">Find rows that contain the words "apple" and "turnover", or "apple" and "strudel" (in any order), but rank "apple turnover" higher than "apple strudel"</div> <div class="explain">Show results that contain the words "apple" and "turnover", or "apple" and "strudel" (in any order), but rank "apple turnover" higher than "apple strudel"</div>
<div class="example">apple*</div> <div class="example">apple*</div>
<div class="explain">Find rows that contain words such as "apple", "apples", "applesauce", or "applet"</div> <div class="explain">Show results that contain words such as "apple", "apples", "applesauce", or "applet"</div>
<div class="example">"some words"</div> <div class="example">"some words"</div>
<div class="explain">Find rows that contain the exact phrase "some words" (for example, rows that contain "some words of wisdom" but not "some noise words")</div> <div class="explain">Show results that contain the exact phrase "some words" (for example, rows that contain "some words of wisdom" but not "some noise words")</div>
</div> </div>
{{/if}}
{{#if (eq appMeta.storageProvider constants.StoreProvider.PostgreSQL)}}
<div class="syntax">
<div class="example">apple | banana</div>
<div class="explain">Show results that contain at either word</div>
<div class="example">apple & banana</div>
<div class="explain">Show results that contain both words</div>
<div class="example">apple !macintosh</div>
<div class="explain">Show results that contain the word "apple" but not "macintosh"</div>
<div class="example">google & (apple | microsoft) & !ibm</div>
<div class="explain">Show results that have "google", either "apple" or "microsoft" but not "ibm"</div>
</div>
{{/if}}
</div> </div>
</div> </div>
{{/layout/middle-zone-sidebar}} {{/layout/middle-zone-sidebar}}

View file

@ -36,6 +36,7 @@ export default Service.extend({
setupMode: false, setupMode: false,
secureMode: false, secureMode: false,
maxTags: 3, maxTags: 3,
storageProvider: '',
// for major.minor semver release detection // for major.minor semver release detection
// for bugfix releases, only admin is made aware of new release and end users see no What's New messaging // for bugfix releases, only admin is made aware of new release and end users see no What's New messaging

View file

@ -12,8 +12,9 @@
package org package org
import ( import (
"github.com/documize/community/core/env"
"time" "time"
"github.com/documize/community/core/env"
) )
// SitemapDocument details a document that can be exposed via Sitemap. // SitemapDocument details a document that can be exposed via Sitemap.
@ -40,4 +41,5 @@ type SiteMeta struct {
Valid bool `json:"valid"` Valid bool `json:"valid"`
ConversionEndpoint string `json:"conversionEndpoint"` ConversionEndpoint string `json:"conversionEndpoint"`
License env.License `json:"license"` License env.License `json:"license"`
Storage env.StoreType `json:"storageProvider"`
} }