Add full text search support using TSV indexes
This commit is contained in:
parent
907ada9263
commit
d1bf87e19c
|
@ -132,6 +132,16 @@ query {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Postgres also supports full text search using a TSV index. Super Graph makes it easy to use this full text search capability using the `search` argument.
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
products(seasrch "amazing") {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Super Graph support complex queries where you can add filters, ordering,offsets and limits on the query.
|
Super Graph support complex queries where you can add filters, ordering,offsets and limits on the query.
|
||||||
|
|
||||||
#### Logical Operators
|
#### Logical Operators
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
class AddSearchColumn < ActiveRecord::Migration[5.1]
|
||||||
|
def self.up
|
||||||
|
add_column :products, :tsv, :tsvector
|
||||||
|
add_index :products, :tsv, using: "gin"
|
||||||
|
|
||||||
|
say_with_time("Adding trigger to update the ts_vector column") do
|
||||||
|
execute <<-SQL
|
||||||
|
CREATE FUNCTION products_tsv_trigger() RETURNS trigger AS $$
|
||||||
|
begin
|
||||||
|
new.tsv :=
|
||||||
|
setweight(to_tsvector('pg_catalog.english', coalesce(new.name,'')), 'A') ||
|
||||||
|
setweight(to_tsvector('pg_catalog.english', coalesce(new.description,'')), 'B');
|
||||||
|
return new;
|
||||||
|
end
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON products FOR EACH ROW EXECUTE PROCEDURE products_tsv_trigger();
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.down
|
||||||
|
say_with_time("Removing trigger to update the tsv column") do
|
||||||
|
execute <<-SQL
|
||||||
|
DROP TRIGGER tsvectorupdate
|
||||||
|
ON products
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
remove_index :products, :tsv
|
||||||
|
remove_column :products, :tsv
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2019_03_22_200743) do
|
ActiveRecord::Schema.define(version: 2019_04_05_042247) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -36,6 +36,8 @@ ActiveRecord::Schema.define(version: 2019_03_22_200743) do
|
||||||
t.bigint "user_id"
|
t.bigint "user_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.tsvector "tsv"
|
||||||
|
t.index ["tsv"], name: "index_products_on_tsv", using: :gin
|
||||||
t.index ["user_id"], name: "index_products_on_user_id"
|
t.index ["user_id"], name: "index_products_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -13,5 +13,6 @@ require (
|
||||||
github.com/sirupsen/logrus v1.4.0
|
github.com/sirupsen/logrus v1.4.0
|
||||||
github.com/spf13/viper v1.3.1
|
github.com/spf13/viper v1.3.1
|
||||||
github.com/valyala/fasttemplate v1.0.1
|
github.com/valyala/fasttemplate v1.0.1
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a
|
||||||
mellium.im/sasl v0.2.1 // indirect
|
mellium.im/sasl v0.2.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
24
psql/psql.go
24
psql/psql.go
|
@ -397,12 +397,28 @@ func (v *selectBlock) renderRelationship(w io.Writer, schema *DBSchema) {
|
||||||
|
|
||||||
func (v *selectBlock) renderWhere(w io.Writer) error {
|
func (v *selectBlock) renderWhere(w io.Writer) error {
|
||||||
if v.sel.Where.Op == qcode.OpEqID {
|
if v.sel.Where.Op == qcode.OpEqID {
|
||||||
col, ok := v.schema.PCols[v.sel.Table]
|
t, err := v.schema.GetTable(v.sel.Table)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return fmt.Errorf("no primary key defined for %s", v.sel.Table)
|
return err
|
||||||
|
}
|
||||||
|
if len(t.PrimaryCol) == 0 {
|
||||||
|
return fmt.Errorf("no primary key column defined for %s", v.sel.Table)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(w, `(("%s") = ('%s'))`, col.Name, v.sel.Where.Val)
|
fmt.Fprintf(w, `(("%s") = ('%s'))`, t.PrimaryCol, v.sel.Where.Val)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.sel.Where.Op == qcode.OpTsQuery {
|
||||||
|
t, err := v.schema.GetTable(v.sel.Table)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(t.TSVCol) == 0 {
|
||||||
|
return fmt.Errorf("no tsv column defined for %s", v.sel.Table)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, `(("%s") @@ to_tsquery('%s'))`, t.TSVCol, v.sel.Where.Val)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -337,6 +337,26 @@ func fetchByID(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func searchQuery(t *testing.T) {
|
||||||
|
gql := `query {
|
||||||
|
products(search: "Amazing") {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "products" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("tsv") @@ to_tsquery('Amazing'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "products_0") AS "done_1337";`
|
||||||
|
|
||||||
|
resSQL, err := compileGQLToPSQL(gql)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resSQL != sql {
|
||||||
|
t.Fatal(errNotExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func aggFunction(t *testing.T) {
|
func aggFunction(t *testing.T) {
|
||||||
gql := `query {
|
gql := `query {
|
||||||
products {
|
products {
|
||||||
|
@ -383,6 +403,7 @@ func TestCompileGQL(t *testing.T) {
|
||||||
t.Run("withWhereIsNull", withWhereIsNull)
|
t.Run("withWhereIsNull", withWhereIsNull)
|
||||||
t.Run("withWhereMultiOr", withWhereMultiOr)
|
t.Run("withWhereMultiOr", withWhereMultiOr)
|
||||||
t.Run("fetchByID", fetchByID)
|
t.Run("fetchByID", fetchByID)
|
||||||
|
t.Run("searchQuery", searchQuery)
|
||||||
t.Run("belongsTo", belongsTo)
|
t.Run("belongsTo", belongsTo)
|
||||||
t.Run("oneToMany", oneToMany)
|
t.Run("oneToMany", oneToMany)
|
||||||
t.Run("manyToMany", manyToMany)
|
t.Run("manyToMany", manyToMany)
|
||||||
|
|
|
@ -18,11 +18,16 @@ type TTKey struct {
|
||||||
type DBSchema struct {
|
type DBSchema struct {
|
||||||
ColMap map[TCKey]*DBColumn
|
ColMap map[TCKey]*DBColumn
|
||||||
ColIDMap map[int]*DBColumn
|
ColIDMap map[int]*DBColumn
|
||||||
PCols map[string]*DBColumn
|
Tables map[string]*DBTableInfo
|
||||||
|
|
||||||
RelMap map[TTKey]*DBRel
|
RelMap map[TTKey]*DBRel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DBTableInfo struct {
|
||||||
|
PrimaryCol string
|
||||||
|
TSVCol string
|
||||||
|
}
|
||||||
|
|
||||||
type RelType int
|
type RelType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -63,7 +68,7 @@ func initSchema() *DBSchema {
|
||||||
return &DBSchema{
|
return &DBSchema{
|
||||||
ColMap: make(map[TCKey]*DBColumn),
|
ColMap: make(map[TCKey]*DBColumn),
|
||||||
ColIDMap: make(map[int]*DBColumn),
|
ColIDMap: make(map[int]*DBColumn),
|
||||||
PCols: make(map[string]*DBColumn),
|
Tables: make(map[string]*DBTableInfo),
|
||||||
RelMap: make(map[TTKey]*DBRel),
|
RelMap: make(map[TTKey]*DBRel),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,6 +76,7 @@ func initSchema() *DBSchema {
|
||||||
func updateSchema(schema *DBSchema, t *DBTable, cols []*DBColumn) {
|
func updateSchema(schema *DBSchema, t *DBTable, cols []*DBColumn) {
|
||||||
// Current table
|
// Current table
|
||||||
ct := strings.ToLower(t.Name)
|
ct := strings.ToLower(t.Name)
|
||||||
|
schema.Tables[ct] = &DBTableInfo{}
|
||||||
|
|
||||||
// Foreign key columns in current table
|
// Foreign key columns in current table
|
||||||
var jcols []*DBColumn
|
var jcols []*DBColumn
|
||||||
|
@ -82,8 +88,11 @@ func updateSchema(schema *DBSchema, t *DBTable, cols []*DBColumn) {
|
||||||
|
|
||||||
for _, c := range cols {
|
for _, c := range cols {
|
||||||
switch {
|
switch {
|
||||||
|
case c.Type == "tsvector":
|
||||||
|
schema.Tables[ct].TSVCol = c.Name
|
||||||
|
|
||||||
case c.PrimaryKey:
|
case c.PrimaryKey:
|
||||||
schema.PCols[ct] = c
|
schema.Tables[ct].PrimaryCol = c.Name
|
||||||
|
|
||||||
case len(c.FKeyTable) != 0:
|
case len(c.FKeyTable) != 0:
|
||||||
if len(c.FKeyColID) == 0 {
|
if len(c.FKeyColID) == 0 {
|
||||||
|
@ -244,3 +253,11 @@ WHERE c.relkind = 'r'::char
|
||||||
|
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) {
|
||||||
|
t, ok := s.Tables[table]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("table info not found '%s'", table)
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
|
@ -85,6 +85,7 @@ const (
|
||||||
OpHasKeyAll
|
OpHasKeyAll
|
||||||
OpIsNull
|
OpIsNull
|
||||||
OpEqID
|
OpEqID
|
||||||
|
OpTsQuery
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t ExpOp) String() string {
|
func (t ExpOp) String() string {
|
||||||
|
@ -139,6 +140,8 @@ func (t ExpOp) String() string {
|
||||||
v = "op-is-null"
|
v = "op-is-null"
|
||||||
case OpEqID:
|
case OpEqID:
|
||||||
v = "op-eq-id"
|
v = "op-eq-id"
|
||||||
|
case OpTsQuery:
|
||||||
|
v = "op-ts-query"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("<%s>", v)
|
return fmt.Sprintf("<%s>", v)
|
||||||
}
|
}
|
||||||
|
@ -341,6 +344,10 @@ func (com *Compiler) compileArgs(sel *Select, args []*Arg) error {
|
||||||
if sel.ID == int16(0) {
|
if sel.ID == int16(0) {
|
||||||
err = com.compileArgID(sel, args[i])
|
err = com.compileArgID(sel, args[i])
|
||||||
}
|
}
|
||||||
|
case "search":
|
||||||
|
if sel.ID == int16(0) {
|
||||||
|
err = com.compileArgSearch(sel, args[i])
|
||||||
|
}
|
||||||
case "where":
|
case "where":
|
||||||
err = com.compileArgWhere(sel, args[i])
|
err = com.compileArgWhere(sel, args[i])
|
||||||
case "orderby", "order_by", "order":
|
case "orderby", "order_by", "order":
|
||||||
|
@ -438,6 +445,25 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) error {
|
||||||
|
if sel.Where != nil && sel.Where.Op == OpTsQuery {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ex := &Exp{
|
||||||
|
Op: OpTsQuery,
|
||||||
|
Type: ValStr,
|
||||||
|
Val: arg.Val.Val,
|
||||||
|
}
|
||||||
|
|
||||||
|
if sel.Where != nil {
|
||||||
|
sel.Where = &Exp{Op: OpAnd, Children: []*Exp{ex, sel.Where}}
|
||||||
|
} else {
|
||||||
|
sel.Where = ex
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg) error {
|
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue