Add full text search support using TSV indexes

This commit is contained in:
Vikram Rangnekar 2019-04-05 01:44:30 -04:00
parent 907ada9263
commit d1bf87e19c
8 changed files with 134 additions and 8 deletions

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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
) )

View File

@ -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
} }

View File

@ -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)

View File

@ -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
}

View File

@ -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