mirror of
https://github.com/documize/community.git
synced 2025-07-19 05:09:42 +02:00
Enable searching for SQL Server storage provider
This commit is contained in:
parent
a41f43c380
commit
e98f7b9218
10 changed files with 1040 additions and 783 deletions
|
@ -13,9 +13,9 @@ All you need to provide is PostgreSQL or any MySQL variant.
|
|||
|
||||
## Latest Release
|
||||
|
||||
[Community Edition: v2.2.1](https://github.com/documize/community/releases)
|
||||
[Community Edition: v2.3.0](https://github.com/documize/community/releases)
|
||||
|
||||
[Enterprise Edition: v2.2.1](https://www.documize.com/downloads)
|
||||
[Enterprise Edition: v2.3.0](https://www.documize.com/downloads)
|
||||
|
||||
> *We provide frequent product updates for both cloud and self-hosted customers.*
|
||||
>
|
||||
|
@ -45,7 +45,7 @@ Heck, Documize will probably run just fine on a Raspberry Pi.
|
|||
- Brave
|
||||
- Vivaldi
|
||||
- Opera
|
||||
- Microsoft Edge (v42+)
|
||||
- Edge (v42+)
|
||||
|
||||
## Technology Stack
|
||||
|
||||
|
|
|
@ -263,20 +263,18 @@ CREATE TABLE dmz_pin (
|
|||
);
|
||||
CREATE INDEX idx_pin_1 ON dmz_pin (c_userid);
|
||||
|
||||
-- DROP TABLE IF EXISTS dmz_search;
|
||||
-- CREATE TABLE dmz_search (
|
||||
-- id BIGINT PRIMARY KEY IDENTITY (1, 1) NOT NULL,
|
||||
-- c_orgid NVARCHAR(20) COLLATE Latin1_General_CS_AS NOT NULL,
|
||||
-- c_docid NVARCHAR(20) COLLATE Latin1_General_CS_AS NOT NULL,
|
||||
-- c_itemid NVARCHAR(20) COLLATE Latin1_General_CS_AS NOT NULL DEFAULT '',
|
||||
-- c_itemtype NVARCHAR(10) COLLATE Latin1_General_CS_AS NOT NULL,
|
||||
-- c_content NVARCHAR(MAX) COLLATE Latin1_General_CS_AS,
|
||||
-- c_token TSVECTOR,
|
||||
-- c_created DATETIME2 NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
-- );
|
||||
-- CREATE INDEX idx_search_1 ON dmz_search (c_orgid);
|
||||
-- CREATE INDEX idx_search_2 ON dmz_search (c_docid);
|
||||
-- CREATE INDEX idx_search_3 ON dmz_search USING GIN(c_token);
|
||||
DROP TABLE IF EXISTS dmz_search;
|
||||
CREATE TABLE dmz_search (
|
||||
id BIGINT PRIMARY KEY IDENTITY (1, 1) NOT NULL,
|
||||
c_orgid NVARCHAR(20) COLLATE Latin1_General_CS_AS NOT NULL,
|
||||
c_docid NVARCHAR(20) COLLATE Latin1_General_CS_AS NOT NULL,
|
||||
c_itemid NVARCHAR(20) COLLATE Latin1_General_CS_AS NOT NULL DEFAULT '',
|
||||
c_itemtype NVARCHAR(10) COLLATE Latin1_General_CS_AS NOT NULL,
|
||||
c_content NVARCHAR(MAX) COLLATE Latin1_General_CS_AS,
|
||||
c_created DATETIME2 NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX idx_search_1 ON dmz_search (c_orgid);
|
||||
CREATE INDEX idx_search_2 ON dmz_search (c_docid);
|
||||
|
||||
DROP TABLE IF EXISTS dmz_section;
|
||||
CREATE TABLE dmz_section (
|
||||
|
|
|
@ -14,7 +14,6 @@ package search
|
|||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/model/attachment"
|
||||
"github.com/documize/community/model/category"
|
||||
|
@ -27,10 +26,6 @@ import (
|
|||
// IndexDocument adds search indesd entries for document inserting title, tags and attachments as
|
||||
// searchable items. Any existing document entries are removed.
|
||||
func (m *Indexer) IndexDocument(ctx domain.RequestContext, d doc.Document, a []attachment.Attachment) {
|
||||
if m.runtime.StoreProvider.Type() == env.StoreTypeSQLServer {
|
||||
return
|
||||
}
|
||||
|
||||
method := "search.IndexDocument"
|
||||
var err error
|
||||
|
||||
|
@ -53,10 +48,6 @@ func (m *Indexer) IndexDocument(ctx domain.RequestContext, d doc.Document, a []a
|
|||
|
||||
// DeleteDocument removes all search entries for document.
|
||||
func (m *Indexer) DeleteDocument(ctx domain.RequestContext, ID string) {
|
||||
if m.runtime.StoreProvider.Type() == env.StoreTypeSQLServer {
|
||||
return
|
||||
}
|
||||
|
||||
method := "search.DeleteDocument"
|
||||
var err error
|
||||
|
||||
|
@ -80,10 +71,6 @@ func (m *Indexer) DeleteDocument(ctx domain.RequestContext, ID string) {
|
|||
// IndexContent adds search index entry for document context.
|
||||
// Any existing document entries are removed.
|
||||
func (m *Indexer) IndexContent(ctx domain.RequestContext, p page.Page) {
|
||||
if m.runtime.StoreProvider.Type() == env.StoreTypeSQLServer {
|
||||
return
|
||||
}
|
||||
|
||||
method := "search.IndexContent"
|
||||
var err error
|
||||
|
||||
|
@ -111,10 +98,6 @@ func (m *Indexer) IndexContent(ctx domain.RequestContext, p page.Page) {
|
|||
|
||||
// DeleteContent removes all search entries for specific document content.
|
||||
func (m *Indexer) DeleteContent(ctx domain.RequestContext, pageID string) {
|
||||
if m.runtime.StoreProvider.Type() == env.StoreTypeSQLServer {
|
||||
return
|
||||
}
|
||||
|
||||
method := "search.DeleteContent"
|
||||
var err error
|
||||
|
||||
|
|
|
@ -336,7 +336,8 @@ func (s Store) matchLike(ctx domain.RequestContext, keywords, itemType string) (
|
|||
|
||||
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,
|
||||
d.c_spaceid as spaceid, COALESCE(d.c_name,'Unknown') AS document, d.c_tags AS tags, d.c_desc AS excerpt,
|
||||
d.c_spaceid as spaceid, COALESCE(d.c_name,'Unknown') AS document, d.c_tags AS tags,
|
||||
d.c_desc AS excerpt, d.c_template AS template, d.c_versionid AS versionid,
|
||||
COALESCE(l.c_name,'Unknown') AS space, d.c_created AS created, d.c_revised AS revised
|
||||
FROM
|
||||
dmz_search s,
|
||||
|
|
281
domain/search/store_sqlserver.go
Normal file
281
domain/search/store_sqlserver.go
Normal file
|
@ -0,0 +1,281 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
package search
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/documize/community/core/stringutil"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/store"
|
||||
"github.com/documize/community/model/attachment"
|
||||
"github.com/documize/community/model/doc"
|
||||
"github.com/documize/community/model/page"
|
||||
"github.com/documize/community/model/search"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// StoreSQLServer provides data access to space information.
|
||||
type StoreSQLServer struct {
|
||||
store.Context
|
||||
store.SearchStorer
|
||||
}
|
||||
|
||||
// IndexDocument adds search index entries for document inserting title, tags and attachments as
|
||||
// searchable items. Any existing document entries are removed.
|
||||
func (s StoreSQLServer) IndexDocument(ctx domain.RequestContext, doc doc.Document, a []attachment.Attachment) (err error) {
|
||||
method := "search.IndexDocument"
|
||||
|
||||
// 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')"),
|
||||
ctx.OrgID, doc.RefID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "execute delete document index entries")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// insert doc title
|
||||
_, 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)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "execute insert document title entry")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// insert doc tags
|
||||
tags := strings.Split(doc.Tags, "#")
|
||||
for _, t := range tags {
|
||||
t = strings.TrimSpace(t)
|
||||
if len(t) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
_, 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)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "execute insert document tag entry")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range a {
|
||||
_, 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)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "execute insert document file entry")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteDocument removes all search entries for document.
|
||||
func (s StoreSQLServer) 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=?"),
|
||||
ctx.OrgID, ID)
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "execute delete document entries")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IndexContent adds search index entry for document context.
|
||||
// Any existing document entries are removed.
|
||||
func (s StoreSQLServer) IndexContent(ctx domain.RequestContext, p page.Page) (err error) {
|
||||
method := "search.IndexContent"
|
||||
|
||||
// remove previous search entries
|
||||
_, 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)
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "execute delete document content entry")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
|
||||
// prepare content
|
||||
content, err := stringutil.HTML(p.Body).Text(false)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "search strip HTML failed")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
_, 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)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "execute insert section content entry")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
|
||||
_, 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)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "execute insert section title entry")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteContent removes all search entries for specific document content.
|
||||
func (s StoreSQLServer) DeleteContent(ctx domain.RequestContext, pageID string) (err error) {
|
||||
method := "search.DeleteContent"
|
||||
|
||||
// remove all search entries
|
||||
_, err = ctx.Transaction.Exec(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_itemid=? AND c_itemtype=?"),
|
||||
ctx.OrgID, pageID, "page")
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "execute delete document content entry")
|
||||
s.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Documents searches the documents that the client is allowed to see, using the keywords search string, then audits that search.
|
||||
// Visible documents include both those in the client's own organization and those that are public, or whose visibility includes the client.
|
||||
func (s StoreSQLServer) Documents(ctx domain.RequestContext, q search.QueryOptions) (results []search.QueryResult, err error) {
|
||||
q.Keywords = strings.TrimSpace(q.Keywords)
|
||||
if len(q.Keywords) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
results = []search.QueryResult{}
|
||||
|
||||
// Match doc names
|
||||
if q.Doc {
|
||||
r1, err1 := s.match(ctx, q.Keywords, "doc")
|
||||
if err1 != nil {
|
||||
err = errors.Wrap(err1, "search document names")
|
||||
return
|
||||
}
|
||||
|
||||
results = append(results, r1...)
|
||||
}
|
||||
|
||||
// Match doc content
|
||||
if q.Content {
|
||||
r2, err2 := s.match(ctx, q.Keywords, "page")
|
||||
if err2 != nil {
|
||||
err = errors.Wrap(err2, "search document content")
|
||||
return
|
||||
}
|
||||
|
||||
results = append(results, r2...)
|
||||
}
|
||||
|
||||
// Match doc tags
|
||||
if q.Tag {
|
||||
r3, err3 := s.match(ctx, q.Keywords, "tag")
|
||||
if err3 != nil {
|
||||
err = errors.Wrap(err3, "search document tag")
|
||||
return
|
||||
}
|
||||
|
||||
results = append(results, r3...)
|
||||
}
|
||||
|
||||
// Match doc attachments
|
||||
if q.Attachment {
|
||||
r4, err4 := s.match(ctx, q.Keywords, "file")
|
||||
if err4 != nil {
|
||||
err = errors.Wrap(err4, "search document attachments")
|
||||
return
|
||||
}
|
||||
|
||||
results = append(results, r4...)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
results = []search.QueryResult{}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s StoreSQLServer) match(ctx domain.RequestContext, keywords, itemType string) (r []search.QueryResult, err error) {
|
||||
// LIKE clause does not like quotes!
|
||||
keywords = strings.Replace(keywords, "'", "", -1)
|
||||
keywords = strings.Replace(keywords, "\"", "", -1)
|
||||
keywords = strings.Replace(keywords, "%", "", -1)
|
||||
keywords = fmt.Sprintf("%%%s%%", strings.ToLower(keywords))
|
||||
|
||||
// s.c_itemid AS itemid
|
||||
|
||||
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,
|
||||
d.c_spaceid as spaceid, COALESCE(d.c_name,'Unknown') AS document, d.c_tags AS tags,
|
||||
d.c_desc AS excerpt, d.c_template AS template, d.c_versionid AS versionid,
|
||||
COALESCE(l.c_name,'Unknown') AS space, d.c_created AS created, d.c_revised AS revised
|
||||
FROM
|
||||
dmz_search s,
|
||||
dmz_doc d
|
||||
LEFT JOIN
|
||||
dmz_space l ON l.c_orgid=d.c_orgid AND l.c_refid = d.c_spaceid
|
||||
WHERE
|
||||
s.c_orgid = ?
|
||||
AND s.c_itemtype = ?
|
||||
AND s.c_docid = d.c_refid
|
||||
AND d.c_spaceid IN
|
||||
(
|
||||
SELECT c_refid FROM dmz_space WHERE c_orgid=? AND c_refid IN
|
||||
(
|
||||
SELECT c_refid from dmz_permission WHERE c_orgid=? AND c_who='user' AND (c_whoid=? OR c_whoid='0') AND c_location='space'
|
||||
UNION ALL
|
||||
SELECT p.c_refid from dmz_permission p LEFT JOIN dmz_group_member r ON p.c_whoid=r.c_groupid WHERE p.c_orgid=? AND p.c_who='role'
|
||||
AND p.c_location='space' AND (r.c_userid=? OR r.c_userid='0')
|
||||
)
|
||||
)
|
||||
AND LOWER(s.c_content) LIKE ?`)
|
||||
|
||||
err = s.Runtime.Db.Select(&r,
|
||||
sql1,
|
||||
ctx.OrgID,
|
||||
itemType,
|
||||
ctx.OrgID,
|
||||
ctx.OrgID,
|
||||
ctx.UserID,
|
||||
ctx.OrgID,
|
||||
ctx.UserID,
|
||||
keywords)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
r = []search.QueryResult{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "search document")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -142,7 +142,7 @@ func SetSQLServerProvider(r *env.Runtime, s *store.Store) {
|
|||
s.Pin = pinStore
|
||||
|
||||
// Search
|
||||
searchStore := search.Store{}
|
||||
searchStore := search.StoreSQLServer{}
|
||||
searchStore.Runtime = r
|
||||
s.Search = searchStore
|
||||
|
||||
|
|
1454
embed/bindata.go
1454
embed/bindata.go
File diff suppressed because one or more lines are too long
|
@ -71,15 +71,6 @@ let constants = EmberObject.extend({
|
|||
ReviewLabel: 'Changes require approval before publication'
|
||||
},
|
||||
|
||||
// Database type
|
||||
StorageProvider: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
|
||||
SQLServer: 'SQLServer',
|
||||
PostgreSQL: 'PostgreSQL',
|
||||
Percona: 'Percona',
|
||||
MariaDB: 'MariaDB',
|
||||
MySQL: 'MySQL',
|
||||
},
|
||||
|
||||
// Document
|
||||
ApprovalType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
|
||||
None: 0,
|
||||
|
@ -145,10 +136,13 @@ let constants = EmberObject.extend({
|
|||
Publish: 7,
|
||||
},
|
||||
|
||||
// Meta
|
||||
// Database type
|
||||
StoreProvider: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
|
||||
MySQL: 'MySQL',
|
||||
PostgreSQL: 'PostgreSQL',
|
||||
Percona: 'Percona',
|
||||
MariaDB: 'MariaDB',
|
||||
SQLServer: 'SQLServer',
|
||||
},
|
||||
|
||||
// Product is where we try to balance the fine line between useful open core
|
||||
|
|
|
@ -37,12 +37,10 @@
|
|||
<i class={{concat "dicon " constants.Icon.Locked}} />
|
||||
<div class="name">Authentication</div>
|
||||
{{/link-to}}
|
||||
{{#if (not-eq appMeta.storageProvider constants.StorageProvider.SQLServer)}}
|
||||
{{#link-to "customize.search" activeClass="selected" class="item" tagName="div"}}
|
||||
<i class={{concat "dicon " constants.Icon.Search}} />
|
||||
<div class="name">Search</div>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
{{#if (eq appMeta.edition constants.Product.EnterpriseEdition)}}
|
||||
{{#link-to "customize.audit" activeClass="selected" class="item" tagName="div"}}
|
||||
<i class={{concat "dicon " constants.Icon.ButtonAction}} />
|
||||
|
|
|
@ -57,6 +57,8 @@
|
|||
<div class="explain">Show results that have "google", either "apple" or "microsoft" but not "ibm"</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (eq appMeta.storageProvider constants.StoreProvider.SQLServer)}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/layout/master-sidebar}}
|
||||
|
@ -64,7 +66,7 @@
|
|||
{{#layout/master-content}}
|
||||
{{layout/logo-heading
|
||||
title="Search"
|
||||
desc="Basic and advanced search queries"
|
||||
desc="Find content"
|
||||
icon=constants.Icon.Search}}
|
||||
|
||||
{{search/search-view
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue