Add control fulltext search

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-06-07 12:02:16 -07:00
parent 032144e04b
commit 67cc9d9ddc
9 changed files with 296 additions and 46 deletions

View File

@@ -64,6 +64,7 @@ func (c *Controls) LoadByDocumentID(
scope Scoper,
documentID gid.GID,
cursor *page.Cursor[ControlOrderField],
filter *ControlFilter,
) error {
q := `
WITH ctrl AS (
@@ -96,11 +97,13 @@ FROM
ctrl
WHERE %s
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"document_id": documentID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
@@ -124,6 +127,7 @@ func (c *Controls) LoadByMeasureID(
scope Scoper,
measureID gid.GID,
cursor *page.Cursor[ControlOrderField],
filter *ControlFilter,
) error {
q := `
WITH ctrl AS (
@@ -156,11 +160,14 @@ FROM
ctrl
WHERE %s
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"measure_id": measureID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
@@ -183,6 +190,7 @@ func (c *Controls) LoadByRiskID(
scope Scoper,
riskID gid.GID,
cursor *page.Cursor[ControlOrderField],
filter *ControlFilter,
) error {
q := `
WITH ctrl AS (
@@ -221,11 +229,13 @@ FROM
ctrl
WHERE %s
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"risk_id": riskID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
@@ -249,6 +259,7 @@ func (c *Controls) LoadByFrameworkID(
scope Scoper,
frameworkID gid.GID,
cursor *page.Cursor[ControlOrderField],
filter *ControlFilter,
) error {
q := `
SELECT
@@ -266,11 +277,14 @@ WHERE
%s
AND framework_id = @framework_id
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"framework_id": frameworkID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
@@ -293,6 +307,7 @@ func (c *Controls) LoadByOrganizationID(
scope Scoper,
organizationID gid.GID,
cursor *page.Cursor[ControlOrderField],
filter *ControlFilter,
) error {
q := `
WITH ctrl AS (
@@ -325,11 +340,14 @@ FROM
ctrl
WHERE %s
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"github.com/jackc/pgx/v5"
)
type (
ControlFilter struct {
query *string
}
)
func NewControlFilter(query *string) *ControlFilter {
return &ControlFilter{
query: query,
}
}
func (f *ControlFilter) SQLArguments() pgx.StrictNamedArgs {
return pgx.StrictNamedArgs{
"query": f.query,
}
}
func (f *ControlFilter) SQLFragment() string {
if f.query == nil || *f.query == "" {
return "TRUE"
}
return "search_vector @@ websearch_to_tsquery('simple', @query)"
}

View File

@@ -0,0 +1,10 @@
ALTER TABLE controls ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
to_tsvector('simple',
COALESCE(section_title, '') || ' ' ||
COALESCE(name, '') || ' ' ||
COALESCE(description, '')
)
) STORED;
CREATE INDEX controls_search_idx ON controls USING gin(search_vector);