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.
|
||||
|
||||
#### 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.
|
||||
|
||||
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
|
||||
enable_extension "plpgsql"
|
||||
|
@ -36,6 +36,8 @@ ActiveRecord::Schema.define(version: 2019_03_22_200743) do
|
|||
t.bigint "user_id"
|
||||
t.datetime "created_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"
|
||||
end
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -13,5 +13,6 @@ require (
|
|||
github.com/sirupsen/logrus v1.4.0
|
||||
github.com/spf13/viper v1.3.1
|
||||
github.com/valyala/fasttemplate v1.0.1
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a
|
||||
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 {
|
||||
if v.sel.Where.Op == qcode.OpEqID {
|
||||
col, ok := v.schema.PCols[v.sel.Table]
|
||||
if !ok {
|
||||
return fmt.Errorf("no primary key defined for %s", v.sel.Table)
|
||||
t, err := v.schema.GetTable(v.sel.Table)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
gql := `query {
|
||||
products {
|
||||
|
@ -383,6 +403,7 @@ func TestCompileGQL(t *testing.T) {
|
|||
t.Run("withWhereIsNull", withWhereIsNull)
|
||||
t.Run("withWhereMultiOr", withWhereMultiOr)
|
||||
t.Run("fetchByID", fetchByID)
|
||||
t.Run("searchQuery", searchQuery)
|
||||
t.Run("belongsTo", belongsTo)
|
||||
t.Run("oneToMany", oneToMany)
|
||||
t.Run("manyToMany", manyToMany)
|
||||
|
|
|
@ -18,11 +18,16 @@ type TTKey struct {
|
|||
type DBSchema struct {
|
||||
ColMap map[TCKey]*DBColumn
|
||||
ColIDMap map[int]*DBColumn
|
||||
PCols map[string]*DBColumn
|
||||
Tables map[string]*DBTableInfo
|
||||
|
||||
RelMap map[TTKey]*DBRel
|
||||
}
|
||||
|
||||
type DBTableInfo struct {
|
||||
PrimaryCol string
|
||||
TSVCol string
|
||||
}
|
||||
|
||||
type RelType int
|
||||
|
||||
const (
|
||||
|
@ -63,7 +68,7 @@ func initSchema() *DBSchema {
|
|||
return &DBSchema{
|
||||
ColMap: make(map[TCKey]*DBColumn),
|
||||
ColIDMap: make(map[int]*DBColumn),
|
||||
PCols: make(map[string]*DBColumn),
|
||||
Tables: make(map[string]*DBTableInfo),
|
||||
RelMap: make(map[TTKey]*DBRel),
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +76,7 @@ func initSchema() *DBSchema {
|
|||
func updateSchema(schema *DBSchema, t *DBTable, cols []*DBColumn) {
|
||||
// Current table
|
||||
ct := strings.ToLower(t.Name)
|
||||
schema.Tables[ct] = &DBTableInfo{}
|
||||
|
||||
// Foreign key columns in current table
|
||||
var jcols []*DBColumn
|
||||
|
@ -82,8 +88,11 @@ func updateSchema(schema *DBSchema, t *DBTable, cols []*DBColumn) {
|
|||
|
||||
for _, c := range cols {
|
||||
switch {
|
||||
case c.Type == "tsvector":
|
||||
schema.Tables[ct].TSVCol = c.Name
|
||||
|
||||
case c.PrimaryKey:
|
||||
schema.PCols[ct] = c
|
||||
schema.Tables[ct].PrimaryCol = c.Name
|
||||
|
||||
case len(c.FKeyTable) != 0:
|
||||
if len(c.FKeyColID) == 0 {
|
||||
|
@ -244,3 +253,11 @@ WHERE c.relkind = 'r'::char
|
|||
|
||||
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
|
||||
OpIsNull
|
||||
OpEqID
|
||||
OpTsQuery
|
||||
)
|
||||
|
||||
func (t ExpOp) String() string {
|
||||
|
@ -139,6 +140,8 @@ func (t ExpOp) String() string {
|
|||
v = "op-is-null"
|
||||
case OpEqID:
|
||||
v = "op-eq-id"
|
||||
case OpTsQuery:
|
||||
v = "op-ts-query"
|
||||
}
|
||||
return fmt.Sprintf("<%s>", v)
|
||||
}
|
||||
|
@ -341,6 +344,10 @@ func (com *Compiler) compileArgs(sel *Select, args []*Arg) error {
|
|||
if sel.ID == int16(0) {
|
||||
err = com.compileArgID(sel, args[i])
|
||||
}
|
||||
case "search":
|
||||
if sel.ID == int16(0) {
|
||||
err = com.compileArgSearch(sel, args[i])
|
||||
}
|
||||
case "where":
|
||||
err = com.compileArgWhere(sel, args[i])
|
||||
case "orderby", "order_by", "order":
|
||||
|
@ -438,6 +445,25 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) error {
|
|||
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 {
|
||||
var err error
|
||||
|
||||
|
|
Loading…
Reference in New Issue