PgSearch สร้างขอบเขตที่มีชื่อซึ่งใช้ประโยชน์จากการค้นหาข้อความแบบเต็มของ PostgreSQL
อ่านบล็อกโพสต์แนะนำ PgSearch ได้ที่ https://tanzu.vmware.com/content/blog/pg-search-how-i-learned-to-stop-worrying-and-love-postgresql-full-text-search
$ gem install pg_search
หรือเพิ่มบรรทัดนี้ใน Gemfile ของคุณ:
gem 'pg_search'
นอกเหนือจากการติดตั้งและการกำหนด gem แล้ว คุณอาจต้องการรวมงาน PgSearch rake ไว้ใน Rakefile ของคุณ สิ่งนี้ไม่จำเป็นสำหรับโปรเจ็กต์ Rails ซึ่งรับงาน Rake ผ่าน Railtie
load "pg_search/tasks.rb"
หากต้องการเพิ่ม PgSearch ให้กับโมเดล Active Record เพียงรวมโมดูล PgSearch
class Shape < ActiveRecord :: Base
include PgSearch :: Model
end
multisearchable
pg_search_scope
:tsearch
(ค้นหาข้อความแบบเต็ม):prefix
(PostgreSQL 8.4 และใหม่กว่าเท่านั้น):negation
:dictionary
:normalization
:any_word
:sort_only
:highlight
:dmetaphone
(ค้นหาเสียงเหมือน Metaphone สองครั้ง):trigram
(ค้นหา Trigram):threshold
:word_similarity
:ranked_by
(การเลือกอัลกอริธึมการจัดอันดับ):order_within_rank
(ทำลายความสัมพันธ์)PgSearch#pg_search_rank
(การอ่านอันดับของบันทึกเป็นแบบลอยตัว)pg_search รองรับสองเทคนิคที่แตกต่างกันสำหรับการค้นหา ขอบเขตการค้นหาหลายรายการ และขอบเขตการค้นหา
เทคนิคแรกคือการค้นหาหลายรายการ ซึ่งสามารถผสมบันทึกของคลาส Active Record ต่างๆ จำนวนมากเข้าด้วยกันเป็นดัชนีการค้นหาส่วนกลางเดียวทั่วทั้งแอปพลิเคชันของคุณ ไซต์ส่วนใหญ่ที่ต้องการสนับสนุนหน้าการค้นหาทั่วไปจะต้องการใช้คุณลักษณะนี้
อีกเทคนิคหนึ่งคือขอบเขตการค้นหา ซึ่งช่วยให้คุณสามารถค้นหาขั้นสูงยิ่งขึ้นกับคลาส Active Record เพียงคลาสเดียวเท่านั้น สิ่งนี้มีประโยชน์มากกว่าสำหรับการสร้างสิ่งต่างๆ เช่น การเติมข้อความอัตโนมัติ หรือการกรองรายการข้อมูลในการค้นหาแบบประกอบ
ก่อนที่จะใช้การค้นหาหลายรายการ คุณต้องสร้างและรันการย้ายข้อมูลเพื่อสร้างตารางฐานข้อมูล pg_search_documents
$ rails g pg_search:migration:multisearch
$ rake db:migrate
หากต้องการเพิ่มโมเดลลงในดัชนีการค้นหาส่วนกลางสำหรับแอปพลิเคชันของคุณ ให้เรียกใช้ multisearchable ในคำจำกัดความคลาส
class EpicPoem < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :title , :author ]
end
class Flower < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : :color
end
หากโมเดลนี้มีเรคคอร์ดอยู่แล้ว คุณจะต้องจัดทำดัชนีโมเดลนี้ใหม่เพื่อรับเรคคอร์ดที่มีอยู่ในตาราง pg_search_documents ดูงานสร้างใหม่ด้านล่าง
เมื่อใดก็ตามที่มีการสร้าง อัปเดต หรือทำลายบันทึก การเรียกกลับของ Active Record จะเริ่มทำงาน ซึ่งนำไปสู่การสร้างบันทึก PgSearch::Document ที่สอดคล้องกันในตาราง pg_search_documents ตัวเลือก :against อาจเป็นหนึ่งหรือหลายวิธีซึ่งจะถูกเรียกใช้ในบันทึกเพื่อสร้างข้อความค้นหา
คุณยังสามารถส่ง Proc หรือชื่อเมธอดเพื่อเรียกเพื่อพิจารณาว่าควรรวมบันทึกใดระเบียนหนึ่งไว้หรือไม่
class Convertible < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :make , :model ] ,
if : :available_in_red?
end
class Jalopy < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :make , :model ] ,
if : lambda { | record | record . model_year > 1970 }
end
โปรดทราบว่าชื่อ Proc หรือเมธอดถูกเรียกใน after_save hook ซึ่งหมายความว่าคุณควรระมัดระวังเมื่อใช้เวลาหรือวัตถุอื่นๆ ในตัวอย่างต่อไปนี้ หากบันทึกครั้งล่าสุดก่อนเวลาประทับที่ Published_at บันทึกนั้นจะไม่ถูกแสดงรายการในการค้นหาทั่วโลกเลยจนกว่าจะแตะอีกครั้งหลังจากการประทับเวลา
class AntipatternExample
include PgSearch :: Model
multisearchable against : [ :contents ] ,
if : :published?
def published?
published_at < Time . now
end
end
problematic_record = AntipatternExample . create! (
contents : "Using :if with a timestamp" ,
published_at : 10 . minutes . from_now
)
problematic_record . published? # => false
PgSearch . multisearch ( "timestamp" ) # => No results
sleep 20 . minutes
problematic_record . published? # => true
PgSearch . multisearch ( "timestamp" ) # => No results
problematic_record . save!
problematic_record . published? # => true
PgSearch . multisearch ( "timestamp" ) # => Includes problematic_record
อัปเดต pg_search_documents แบบมีเงื่อนไข
คุณยังสามารถใช้ตัวเลือก :update_if
เพื่อส่ง Proc หรือชื่อเมธอดที่จะเรียกเพื่อพิจารณาว่าควรอัปเดตบันทึกใดโดยเฉพาะหรือไม่
โปรดทราบว่าชื่อ Proc หรือเมธอดถูกเรียกใน after_save
hook ดังนั้นหากคุณใช้แฟล็กสกปรก ActiveRecord ให้ใช้ *_previously_changed?
-
class Message < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :body ] ,
update_if : :body_previously_changed?
end
ระบุแอ็ตทริบิวต์เพิ่มเติมที่จะบันทึกลงในตาราง pg_search_documents
คุณสามารถระบุ :additional_attributes
ที่จะบันทึกภายในตาราง pg_search_documents
ตัวอย่างเช่น บางทีคุณกำลังจัดทำดัชนีโมเดลหนังสือและโมเดลบทความ และต้องการรวม author_id
ขั้นแรก เราต้องเพิ่มการอ้างอิงถึงผู้เขียนในการย้ายข้อมูลโดยสร้างตาราง pg_search_documents
ของเรา
create_table :pg_search_documents do | t |
t . text :content
t . references :author , index : true
t . belongs_to :searchable , polymorphic : true , index : true
t . timestamps null : false
end
จากนั้นเราสามารถส่งแอตทริบิวต์เพิ่มเติมนี้ในแลมบ์ดาได้
multisearchable (
against : [ :title , :body ] ,
additional_attributes : -> ( article ) { { author_id : article . author_id } }
)
ซึ่งช่วยให้สามารถค้นหาได้เร็วขึ้นมากโดยไม่ต้องรวมในภายหลังโดยทำสิ่งต่อไปนี้:
PgSearch . multisearch ( params [ 'search' ] ) . where ( author_id : 2 )
หมายเหตุ: ขณะนี้คุณต้องเรียก record.update_pg_search_document
ด้วยตนเองเพื่อให้แอตทริบิวต์เพิ่มเติมที่จะรวมไว้ในตาราง pg_search_documents
การเชื่อมโยงสองรายการจะถูกสร้างขึ้นโดยอัตโนมัติ ในบันทึกต้นฉบับ จะมีการเชื่อมโยง has_one :pg_search_document ที่ชี้ไปที่บันทึก PgSearch::Document และในบันทึก PgSearch::Document จะมีการเชื่อมโยงเป็นของ allowance_to :searchable polymorphic ที่ชี้กลับไปยังบันทึกต้นฉบับ
odyssey = EpicPoem . create! ( title : "Odyssey" , author : "Homer" )
search_document = odyssey . pg_search_document #=> PgSearch::Document instance
search_document . searchable #=> #<EpicPoem id: 1, title: "Odyssey", author: "Homer">
หากต้องการดึงข้อมูลรายการ PgSearch::Document สำหรับบันทึกทั้งหมดที่ตรงกับคำค้นหาที่กำหนด ให้ใช้ PgSearch.multisearch
odyssey = EpicPoem . create! ( title : "Odyssey" , author : "Homer" )
rose = Flower . create! ( color : "Red" )
PgSearch . multisearch ( "Homer" ) #=> [#<PgSearch::Document searchable: odyssey>]
PgSearch . multisearch ( "Red" ) #=> [#<PgSearch::Document searchable: rose>]
PgSearch.multisearch ส่งคืน ActiveRecord::Relation เช่นเดียวกับขอบเขต เพื่อให้คุณสามารถโยงการเรียกขอบเขตไปยังจุดสิ้นสุดได้ วิธีนี้ใช้ได้กับอัญมณีอย่างคามินาริที่เพิ่มวิธีขอบเขต เช่นเดียวกับขอบเขตทั่วไป ฐานข้อมูลจะได้รับคำขอ SQL เมื่อจำเป็นเท่านั้น
PgSearch . multisearch ( "Bertha" ) . limit ( 10 )
PgSearch . multisearch ( "Juggler" ) . where ( searchable_type : "Occupation" )
PgSearch . multisearch ( "Alamo" ) . page ( 3 ) . per ( 30 )
PgSearch . multisearch ( "Diagonal" ) . find_each do | document |
puts document . searchable . updated_at
end
PgSearch . multisearch ( "Moro" ) . reorder ( "" ) . group ( :searchable_type ) . count ( :all )
PgSearch . multisearch ( "Square" ) . includes ( :searchable )
PgSearch.multisearch สามารถกำหนดค่าได้โดยใช้ตัวเลือกเดียวกันกับ pg_search_scope
(อธิบายรายละเอียดเพิ่มเติมด้านล่าง) เพียงตั้งค่า PgSearch.multisearch_options ในเครื่องมือเริ่มต้น:
PgSearch . multisearch_options = {
using : [ :tsearch , :trigram ] ,
ignoring : :accents
}
หากคุณเปลี่ยนตัวเลือก :against ในคลาส เพิ่ม multisearchable ให้กับคลาสที่มีบันทึกในฐานข้อมูลอยู่แล้ว หรือลบ multisearchable ออกจากคลาสเพื่อลบออกจากดัชนี คุณจะพบว่าตาราง pg_search_documents อาจกลายเป็น out- ของการซิงค์กับบันทึกจริงในตารางอื่นๆ ของคุณ
ดัชนีอาจไม่ซิงค์กันหากคุณเคยแก้ไขระเบียนในลักษณะที่ไม่ทริกเกอร์การเรียกกลับของ Active Record ตัวอย่างเช่น เมธอดอินสแตนซ์ #update_attribute และเมธอดคลาส .update_all ข้ามการโทรกลับและแก้ไขฐานข้อมูลโดยตรง
หากต้องการลบเอกสารทั้งหมดสำหรับชั้นเรียนที่กำหนด คุณสามารถลบบันทึก PgSearch::Document ทั้งหมดได้
PgSearch :: Document . delete_by ( searchable_type : "Animal" )
หากต้องการสร้างเอกสารใหม่สำหรับคลาสที่กำหนด ให้รัน:
PgSearch :: Multisearch . rebuild ( Product )
วิธี rebuild
จะลบเอกสารทั้งหมดสำหรับคลาสที่กำหนดก่อนที่จะสร้างใหม่ ในบางสถานการณ์ สิ่งนี้อาจไม่เป็นที่ต้องการ เช่น เมื่อคุณใช้การสืบทอดตารางเดียวและ searchable_type
เป็นคลาสพื้นฐานของคุณ คุณสามารถป้องกันไม่ให้ rebuild
ลบบันทึกของคุณได้ดังนี้:
PgSearch :: Multisearch . rebuild ( Product , clean_up : false )
rebuild
ภายในธุรกรรมเดียว หากต้องการทำงานนอกธุรกรรม คุณสามารถส่งผ่าน transactional: false
ดังนี้:
PgSearch :: Multisearch . rebuild ( Product , transactional : false )
การสร้างใหม่ยังมีให้ใช้งานเป็นงานคราดเพื่อความสะดวก
$ rake pg_search:multisearch:rebuild[BlogPost]
อาร์กิวเมนต์ทางเลือกที่สองสามารถส่งผ่านเพื่อระบุพาธการค้นหาสคีมา PostgreSQL ที่จะใช้ สำหรับฐานข้อมูลหลายผู้เช่าที่มีตาราง pg_search_documents หลายตาราง ต่อไปนี้จะตั้งค่าเส้นทางการค้นหาสคีมาเป็น "my_schema" ก่อนที่จะจัดทำดัชนีใหม่
$ rake pg_search:multisearch:rebuild[BlogPost,my_schema]
สำหรับโมเดลที่สามารถค้นหาได้หลายรายการ :against
เมธอดที่แมปกับแอ็ตทริบิวต์ Active Record โดยตรง คำสั่ง SQL เดี่ยวที่มีประสิทธิภาพจะถูกรันเพื่ออัพเดตตาราง pg_search_documents
ทั้งหมดในครั้งเดียว อย่างไรก็ตาม หากคุณเรียกใช้เมธอดไดนามิกใดๆ ใน :against
ดังนั้น update_pg_search_document
จะถูกเรียกใช้บนเรคคอร์ดแต่ละรายการที่ได้รับการจัดทำดัชนีเป็นชุด
คุณยังสามารถจัดให้มีการใช้งานที่กำหนดเองสำหรับการสร้างเอกสารใหม่โดยการเพิ่มเมธอดคลาสที่เรียกว่า rebuild_pg_search_documents
ให้กับโมเดลของคุณ
class Movie < ActiveRecord :: Base
belongs_to :director
def director_name
director . name
end
multisearchable against : [ :name , :director_name ]
# Naive approach
def self . rebuild_pg_search_documents
find_each { | record | record . update_pg_search_document }
end
# More sophisticated approach
def self . rebuild_pg_search_documents
connection . execute <<~SQL . squish
INSERT INTO pg_search_documents (searchable_type, searchable_id, content, created_at, updated_at)
SELECT 'Movie' AS searchable_type,
movies.id AS searchable_id,
CONCAT_WS(' ', movies.name, directors.name) AS content,
now() AS created_at,
now() AS updated_at
FROM movies
LEFT JOIN directors
ON directors.id = movies.director_id
SQL
end
end
หมายเหตุ: หากใช้ PostgreSQL ก่อน 9.1 ให้แทนที่การเรียกใช้ฟังก์ชัน CONCAT_WS()
ด้วยการต่อข้อมูลแบบ double-pipe เช่น (movies.name || ' ' || directors.name)
อย่างไรก็ตาม โปรดทราบว่าหากค่าที่รวมเข้าด้วย กัน เป็น NULL ค่า content
สุดท้ายก็จะเป็น NULL เช่นกัน ในขณะที่ CONCAT_WS()
จะเพิกเฉยต่อค่า NULL แบบเลือก
หากคุณมีการดำเนินการจำนวนมากที่ต้องทำ เช่น การนำเข้าเรกคอร์ดจำนวนมากจากแหล่งภายนอก คุณอาจต้องการเพิ่มความเร็วโดยการปิดการทำดัชนีชั่วคราว จากนั้นคุณสามารถใช้เทคนิคข้อใดข้อหนึ่งข้างต้นเพื่อสร้างเอกสารการค้นหาใหม่แบบออฟไลน์
PgSearch . disable_multisearch do
Movie . import_from_xml_file ( File . open ( "movies.xml" ) )
end
คุณสามารถใช้ pg_search_scope เพื่อสร้างขอบเขตการค้นหา พารามิเตอร์ตัวแรกคือชื่อขอบเขต และพารามิเตอร์ตัวที่สองคือแฮชของตัวเลือก ตัวเลือกที่จำเป็นเพียงอย่างเดียวคือ :against ซึ่งจะบอก pg_search_scope ว่าคอลัมน์ใดหรือคอลัมน์ใดที่จะค้นหา
หากต้องการค้นหาเทียบกับคอลัมน์ ให้ส่งสัญลักษณ์เป็นตัวเลือก :ต่อ
class BlogPost < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_title , against : :title
end
ขณะนี้เรามีขอบเขต ActiveRecord ชื่อ search_by_title ในโมเดล BlogPost ของเรา ต้องใช้พารามิเตอร์ตัวเดียว นั่นคือสตริงคำค้นหา
BlogPost . create! ( title : "Recent Developments in the World of Pastrami" )
BlogPost . create! ( title : "Prosciutto and You: A Retrospective" )
BlogPost . search_by_title ( "pastrami" ) # => [#<BlogPost id: 2, title: "Recent Developments in the World of Pastrami">]
เพียงส่งอาร์เรย์หากคุณต้องการค้นหามากกว่าหนึ่งคอลัมน์
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_full_name , against : [ :first_name , :last_name ]
end
ตอนนี้คำค้นหาของเราสามารถจับคู่คอลัมน์ใดคอลัมน์หนึ่งหรือทั้งสองคอลัมน์ได้
person_1 = Person . create! ( first_name : "Grant" , last_name : "Hill" )
person_2 = Person . create! ( first_name : "Hugh" , last_name : "Grant" )
Person . search_by_full_name ( "Grant" ) # => [person_1, person_2]
Person . search_by_full_name ( "Grant Hill" ) # => [person_1]
เช่นเดียวกับขอบเขตที่มีชื่อ Active Record คุณสามารถส่งผ่านออบเจ็กต์ Proc ที่ส่งคืนแฮชของตัวเลือกได้ ตัวอย่างเช่น ขอบเขตต่อไปนี้ใช้พารามิเตอร์ที่เลือกคอลัมน์ที่จะค้นหาแบบไดนามิก
สำคัญ: แฮชที่ส่งคืนต้องมีคีย์ :query ค่าของมันไม่จำเป็นต้องเป็นไดนามิก คุณสามารถเลือกที่จะฮาร์ดโค้ดให้เป็นค่าเฉพาะได้หากต้องการ
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_name , lambda { | name_part , query |
raise ArgumentError unless [ :first , :last ] . include? ( name_part )
{
against : name_part ,
query : query
}
}
end
person_1 = Person . create! ( first_name : "Grant" , last_name : "Hill" )
person_2 = Person . create! ( first_name : "Hugh" , last_name : "Grant" )
Person . search_by_name :first , "Grant" # => [person_1]
Person . search_by_name :last , "Grant" # => [person_2]
สามารถค้นหาคอลัมน์ตามโมเดลที่เกี่ยวข้องได้ โปรดทราบว่าหากคุณทำเช่นนี้ จะไม่สามารถเร่งความเร็วการค้นหาด้วยดัชนีฐานข้อมูลได้ อย่างไรก็ตาม ได้รับการรองรับว่าเป็นวิธีที่รวดเร็วในการลองใช้การค้นหาข้ามโมเดล
คุณสามารถส่งแฮชไปยังตัวเลือก :associated_against เพื่อตั้งค่าการค้นหาผ่านการเชื่อมโยง คีย์คือชื่อของการเชื่อมโยงและค่าทำงานเหมือนกับตัวเลือก :against สำหรับรุ่นอื่น ขณะนี้ ไม่สนับสนุนการค้นหาที่ลึกกว่าการเชื่อมโยงเดียว คุณสามารถแก้ไขปัญหานี้ได้โดยตั้งค่าชุดของ :through การเชื่อมโยงเพื่อชี้ไปตลอดทาง
class Cracker < ActiveRecord :: Base
has_many :cheeses
end
class Cheese < ActiveRecord :: Base
end
class Salami < ActiveRecord :: Base
include PgSearch :: Model
belongs_to :cracker
has_many :cheeses , through : :cracker
pg_search_scope :tasty_search , associated_against : {
cheeses : [ :kind , :brand ] ,
cracker : :kind
}
end
salami_1 = Salami . create!
salami_2 = Salami . create!
salami_3 = Salami . create!
limburger = Cheese . create! ( kind : "Limburger" )
brie = Cheese . create! ( kind : "Brie" )
pepper_jack = Cheese . create! ( kind : "Pepper Jack" )
Cracker . create! ( kind : "Black Pepper" , cheeses : [ brie ] , salami : salami_1 )
Cracker . create! ( kind : "Ritz" , cheeses : [ limburger , pepper_jack ] , salami : salami_2 )
Cracker . create! ( kind : "Graham" , cheeses : [ limburger ] , salami : salami_3 )
Salami . tasty_search ( "pepper" ) # => [salami_1, salami_2]
ตามค่าเริ่มต้น pg_search_scope จะใช้การค้นหาข้อความ PostgreSQL ในตัว หากคุณผ่านตัวเลือก :using ไปที่ pg_search_scope คุณสามารถเลือกเทคนิคการค้นหาอื่นๆ ได้
class Beer < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_name , against : :name , using : [ :tsearch , :trigram , :dmetaphone ]
end
นี่คือตัวอย่างหากคุณส่งผ่าน :using ตัวเลือกหลายรายการพร้อมกับการกำหนดค่าเพิ่มเติม
class Beer < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_name ,
against : :name ,
using : {
:trigram => { } ,
:dmetaphone => { } ,
:tsearch => { :prefix => true }
}
end
คุณสมบัติที่นำมาใช้ในปัจจุบันคือ
การค้นหาข้อความแบบเต็มในตัวของ PostgreSQL รองรับการถ่วงน้ำหนัก การค้นหาคำนำหน้า และการแยกส่วนในหลายภาษา
แต่ละคอลัมน์ที่ค้นหาได้สามารถกำหนดน้ำหนักเป็น "A", "B", "C" หรือ "D" คอลัมน์ที่มีตัวอักษรก่อนหน้าจะมีน้ำหนักมากกว่าคอลัมน์ที่มีตัวอักษรหลัง ดังนั้น ในตัวอย่างต่อไปนี้ ชื่อเรื่องจึงมีความสำคัญที่สุด ตามด้วยคำบรรยาย และสุดท้ายคือเนื้อหา
class NewsArticle < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_full_text , against : {
title : 'A' ,
subtitle : 'B' ,
content : 'C'
}
end
คุณยังสามารถส่งผ่านน้ำหนักในรูปแบบอาร์เรย์ของอาร์เรย์ หรือโครงสร้างอื่นๆ ที่ตอบสนองต่อ #each และให้ผลลัพธ์เป็นสัญลักษณ์เดียวหรือสัญลักษณ์และน้ำหนักก็ได้ หากคุณละเว้นน้ำหนัก ระบบจะใช้ค่าเริ่มต้น
class NewsArticle < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_full_text , against : [
[ :title , 'A' ] ,
[ :subtitle , 'B' ] ,
[ :content , 'C' ]
]
end
class NewsArticle < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_full_text , against : [
[ :title , 'A' ] ,
{ subtitle : 'B' } ,
:content
]
end
การค้นหาข้อความแบบเต็มของ PostgreSQL จะจับคู่กับคำทั้งหมดตามค่าเริ่มต้น หากคุณต้องการค้นหาคำบางส่วน คุณสามารถตั้งค่า :prefix เป็น true ได้ เนื่องจากนี่คือตัวเลือกเฉพาะ :tsearch คุณจึงควรส่งต่อไปยัง :tsearch โดยตรง ดังที่แสดงในตัวอย่างต่อไปนี้
class Superhero < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :whose_name_starts_with ,
against : :name ,
using : {
tsearch : { prefix : true }
}
end
batman = Superhero . create name : 'Batman'
batgirl = Superhero . create name : 'Batgirl'
robin = Superhero . create name : 'Robin'
Superhero . whose_name_starts_with ( "Bat" ) # => [batman, batgirl]
การค้นหาข้อความแบบเต็มของ PostgreSQL จะตรงกับคำค้นหาทั้งหมดตามค่าเริ่มต้น หากคุณต้องการยกเว้นคำบางคำ คุณสามารถตั้งค่า :negation ให้เป็นจริงได้ แล้วคำใดๆ ที่ขึ้นต้นด้วยเครื่องหมายอัศเจรีย์ !
จะถูกแยกออกจากผลลัพธ์ เนื่องจากนี่คือตัวเลือกเฉพาะ :tsearch คุณจึงควรส่งต่อไปยัง :tsearch โดยตรง ดังที่แสดงในตัวอย่างต่อไปนี้
โปรดทราบว่าการรวมสิ่งนี้เข้ากับคุณลักษณะการค้นหาอื่นๆ อาจทำให้เกิดผลลัพธ์ที่ไม่คาดคิดได้ ตัวอย่างเช่น การค้นหา :trigram ไม่มีแนวคิดเกี่ยวกับคำที่แยกออก ดังนั้นหากคุณใช้ทั้ง :tsearch และ :trigram ควบคู่กัน คุณอาจยังคงพบผลลัพธ์ที่มีคำที่คุณพยายามแยกออก
class Animal < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :with_name_matching ,
against : :name ,
using : {
tsearch : { negation : true }
}
end
one_fish = Animal . create ( name : "one fish" )
two_fish = Animal . create ( name : "two fish" )
red_fish = Animal . create ( name : "red fish" )
blue_fish = Animal . create ( name : "blue fish" )
Animal . with_name_matching ( "fish !red !blue" ) # => [one_fish, two_fish]
การค้นหาข้อความแบบเต็มของ PostgreSQL ยังรองรับพจนานุกรมหลายพจนานุกรมสำหรับการแยกแยะ คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับวิธีการทำงานของพจนานุกรมได้โดยการอ่านเอกสาร PostgreSQL หากคุณใช้พจนานุกรมภาษาใดภาษาหนึ่ง เช่น "ภาษาอังกฤษ" รูปแบบคำต่างๆ (เช่น "กระโดด" และ "กระโดด") จะจับคู่กัน หากคุณไม่ต้องการกั้นพจนานุกรม คุณควรเลือกพจนานุกรม "แบบง่าย" ที่ไม่ทำการกั้นใดๆ หากคุณไม่ระบุพจนานุกรม ระบบจะใช้พจนานุกรม "แบบง่าย"
class BoringTweet < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :kinda_matching ,
against : :text ,
using : {
tsearch : { dictionary : "english" }
}
pg_search_scope :literally_matching ,
against : :text ,
using : {
tsearch : { dictionary : "simple" }
}
end
sleep = BoringTweet . create! text : "I snoozed my alarm for fourteen hours today. I bet I can beat that tomorrow! #sleep"
sleeping = BoringTweet . create! text : "You know what I like? Sleeping. That's what. #enjoyment"
sleeps = BoringTweet . create! text : "In the jungle, the mighty jungle, the lion sleeps #tonight"
BoringTweet . kinda_matching ( "sleeping" ) # => [sleep, sleeping, sleeps]
BoringTweet . literally_matching ( "sleeps" ) # => [sleeps]
PostgreSQL รองรับอัลกอริธึมหลายตัวสำหรับการจัดอันดับผลลัพธ์เทียบกับข้อความค้นหา ตัวอย่างเช่น คุณอาจต้องการพิจารณาขนาดเอกสารโดยรวมหรือระยะห่างระหว่างคำค้นหาหลายคำในข้อความต้นฉบับ ตัวเลือกนี้รับจำนวนเต็มซึ่งจะถูกส่งโดยตรงไปยัง PostgreSQL ตามเอกสาร PostgreSQL ล่าสุด อัลกอริธึมที่รองรับคือ:
0 (the default) ignores the document length
1 divides the rank by 1 + the logarithm of the document length
2 divides the rank by the document length
4 divides the rank by the mean harmonic distance between extents
8 divides the rank by the number of unique words in document
16 divides the rank by 1 + the logarithm of the number of unique words in document
32 divides the rank by itself + 1
จำนวนเต็มนี้เป็นบิตมาสก์ ดังนั้นหากคุณต้องการรวมอัลกอริธึม คุณสามารถเพิ่มตัวเลขเข้าด้วยกันได้ (เช่น ในการใช้อัลกอริธึม 1, 8 และ 32 คุณจะต้องผ่าน 1 + 8 + 32 = 41)
class BigLongDocument < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :regular_search ,
against : :text
pg_search_scope :short_search ,
against : :text ,
using : {
tsearch : { normalization : 2 }
}
long = BigLongDocument . create! ( text : "Four score and twenty years ago" )
short = BigLongDocument . create! ( text : "Four score" )
BigLongDocument . regular_search ( "four score" ) #=> [long, short]
BigLongDocument . short_search ( "four score" ) #=> [short, long]
การตั้งค่าแอตทริบิวต์นี้เป็นจริงจะทำการค้นหาซึ่งจะส่งคืนโมเดลทั้งหมดที่มีคำใดๆ ในคำค้นหา
class Number < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_any_word ,
against : :text ,
using : {
tsearch : { any_word : true }
}
pg_search_scope :search_all_words ,
against : :text
end
one = Number . create! text : 'one'
two = Number . create! text : 'two'
three = Number . create! text : 'three'
Number . search_any_word ( 'one two three' ) # => [one, two, three]
Number . search_all_words ( 'one two three' ) # => []
การตั้งค่าแอตทริบิวต์นี้เป็นจริงจะทำให้คุณลักษณะนี้พร้อมสำหรับการเรียงลำดับ แต่จะไม่รวมไว้ในเงื่อนไข WHERE ของการสืบค้น
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search ,
against : :name ,
using : {
tsearch : { any_word : true } ,
dmetaphone : { any_word : true , sort_only : true }
}
end
exact = Person . create! ( name : 'ash hines' )
one_exact_one_close = Person . create! ( name : 'ash heinz' )
one_exact = Person . create! ( name : 'ash smith' )
one_close = Person . create! ( name : 'leigh heinz' )
Person . search ( 'ash hines' ) # => [exact, one_exact_one_close, one_exact]
การเพิ่ม .with_pg_search_highlight หลัง pg_search_scope คุณสามารถเข้าถึงแอตทริบิวต์ pg_highlight
สำหรับแต่ละวัตถุได้
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search ,
against : :bio ,
using : {
tsearch : {
highlight : {
StartSel : '<b>' ,
StopSel : '</b>' ,
MaxWords : 123 ,
MinWords : 456 ,
ShortWord : 4 ,
HighlightAll : true ,
MaxFragments : 3 ,
FragmentDelimiter : '…'
}
}
}
end
Person . create! ( :bio => "Born in rural Alberta, where the buffalo roam." )
first_match = Person . search ( "Alberta" ) . with_pg_search_highlight . first
first_match . pg_search_highlight # => "Born in rural <b>Alberta</b>, where the buffalo roam."
ตัวเลือกไฮไลต์ยอมรับตัวเลือกทั้งหมดที่รองรับโดย ts_headline และใช้ค่าเริ่มต้นของ PostgreSQL
ดูเอกสารประกอบสำหรับรายละเอียดเกี่ยวกับความหมายของแต่ละตัวเลือก
Double Metaphone เป็นอัลกอริธึมสำหรับจับคู่คำที่ฟังดูเหมือนกันแม้ว่าจะสะกดต่างกันมากก็ตาม ตัวอย่างเช่น "Geoff" และ "Jeff" มีเสียงเหมือนกันจึงตรงกัน ปัจจุบันนี้ไม่ใช่เมตาโฟนคู่ที่แท้จริง เนื่องจากมีเพียงเมตาโฟนตัวแรกเท่านั้นที่ใช้สำหรับการค้นหา
ขณะนี้การสนับสนุน Double Metaphone มีให้บริการโดยเป็นส่วนหนึ่งของส่วนขยาย fuzzystrmatch ที่ต้องติดตั้งก่อนจึงจะสามารถใช้คุณลักษณะนี้ได้ นอกจากส่วนขยายแล้ว คุณต้องติดตั้งฟังก์ชันยูทิลิตี้ลงในฐานข้อมูลของคุณ หากต้องการสร้างและดำเนินการย้ายข้อมูล ให้รัน:
$ rails g pg_search:migration:dmetaphone
$ rake db:migrate
ตัวอย่างต่อไปนี้แสดงวิธีการใช้งาน :dmetaphone
class Word < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :that_sounds_like ,
against : :spelling ,
using : :dmetaphone
end
four = Word . create! spelling : 'four'
far = Word . create! spelling : 'far'
fur = Word . create! spelling : 'fur'
five = Word . create! spelling : 'five'
Word . that_sounds_like ( "fir" ) # => [four, far, fur]
การค้นหาแบบไตรแกรมทำงานโดยการนับจำนวนสตริงย่อยสามตัวอักษร (หรือ "ไตรแกรม") ที่ตรงกันระหว่างข้อความค้นหาและข้อความ ตัวอย่างเช่น สตริง "Lorem ipsum" สามารถแบ่งออกเป็นไตรแกรมต่อไปนี้:
[" Lo", "Lor", "ore", "rem", "em ", "m i", " ip", "ips", "psu", "sum", "um ", "m "]
การค้นหาแบบ Trigram มีความสามารถบางอย่างในการทำงานแม้ว่าจะมีการพิมพ์ผิดหรือสะกดผิดในข้อความค้นหาหรือข้อความก็ตาม
ขณะนี้การสนับสนุน Trigram พร้อมใช้งานโดยเป็นส่วนหนึ่งของส่วนขยาย pg_trgm ที่ต้องติดตั้งก่อนจึงจะสามารถใช้คุณลักษณะนี้ได้
class Website < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :kinda_spelled_like ,
against : :name ,
using : :trigram
end
yahooo = Website . create! name : "Yahooo!"
yohoo = Website . create! name : "Yohoo!"
gogle = Website . create! name : "Gogle"
facebook = Website . create! name : "Facebook"
Website . kinda_spelled_like ( "Yahoo!" ) # => [yahooo, yohoo]
ตามค่าเริ่มต้น การค้นหาแบบไตรแกรมจะค้นหาบันทึกที่มีความคล้ายคลึงกันอย่างน้อย 0.3 โดยใช้การคำนวณของ pg_trgm คุณสามารถระบุเกณฑ์ที่กำหนดเองได้หากต้องการ ตัวเลขที่สูงกว่าจะตรงกันมากกว่า และทำให้ได้ผลลัพธ์น้อยลง ตัวเลขที่ต่ำกว่าจะตรงกันมากกว่าโดยให้ผลลัพธ์มากกว่า โปรดทราบว่าการตั้งค่าเกณฑ์ Trigram จะบังคับให้มีการสแกนตาราง เนื่องจากแบบสอบถามที่ได้รับใช้ฟังก์ชัน similarity()
แทนตัวดำเนินการ %
class Vegetable < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :strictly_spelled_like ,
against : :name ,
using : {
trigram : {
threshold : 0.5
}
}
pg_search_scope :roughly_spelled_like ,
against : :name ,
using : {
trigram : {
threshold : 0.1
}
}
end
cauliflower = Vegetable . create! name : "cauliflower"
Vegetable . roughly_spelled_like ( "couliflower" ) # => [cauliflower]
Vegetable . strictly_spelled_like ( "couliflower" ) # => [cauliflower]
Vegetable . roughly_spelled_like ( "collyflower" ) # => [cauliflower]
Vegetable . strictly_spelled_like ( "collyflower" ) # => []
ช่วยให้คุณจับคู่คำในสตริงที่ยาวขึ้น ตามค่าเริ่มต้น การค้นหาตรีแกรมจะใช้ %
หรือ similarity()
เป็นค่าความคล้ายคลึงกัน ตั้งค่า word_similarity
เป็น true
เพื่อเลือกใช้ <%
และ word_similarity
แทน ซึ่งทำให้การค้นหาตรีแกรมใช้ความคล้ายคลึงกันของคำค้นหาและคำที่มีความคล้ายคลึงกันมากที่สุด
class Sentence < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :similarity_like ,
against : :name ,
using : {
trigram : {
word_similarity : true
}
}
pg_search_scope :word_similarity_like ,
against : :name ,
using : [ :trigram ]
end
sentence = Sentence . create! name : "Those are two words."
Sentence . similarity_like ( "word" ) # => []
Sentence . word_similarity_like ( "word" ) # => [sentence]
บางครั้งเมื่อทำการสืบค้นรวมคุณสมบัติที่แตกต่างกัน คุณอาจต้องการค้นหาเฉพาะบางฟิลด์ที่มีคุณสมบัติบางอย่าง ตัวอย่างเช่น บางทีคุณอาจต้องการค้นหาแบบไตรแกรมกับฟิลด์ที่สั้นกว่าเท่านั้น เพื่อที่คุณจะได้ไม่ต้องลดเกณฑ์มากเกินไป คุณสามารถระบุฟิลด์ได้โดยใช้ตัวเลือก 'เท่านั้น':
class Image < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :combined_search ,
against : [ :file_name , :short_description , :long_description ]
using : {
tsearch : { dictionary : 'english' } ,
trigram : {
only : [ :file_name , :short_description ]
}
}
end
ตอนนี้คุณสามารถดึงข้อมูลรูปภาพด้วย file_name: 'image_foo.jpg' และ long_description: 'คำอธิบายนี้ยาวมากจนทำให้การค้นหาแบบไตรแกรมล้มเหลวตามขีดจำกัดเกณฑ์ที่สมเหตุสมผล' ด้วย:
Image . combined_search ( 'reasonable' ) # found with tsearch
Image . combined_search ( 'foo' ) # found with trigram
โดยส่วนใหญ่คุณจะต้องการละเว้นเครื่องหมายเน้นเสียงเมื่อค้นหา ซึ่งทำให้สามารถค้นหาคำเช่น "piñata" เมื่อค้นหาด้วยคำค้นหา "pinata" หากคุณตั้งค่า pg_search_scope ให้ละเว้นสำเนียง มันจะละเว้นสำเนียงทั้งในข้อความที่ค้นหาได้และเงื่อนไขการค้นหา
การละเว้นสำเนียงจะใช้ส่วนขยายที่ไม่เน้นเสียงซึ่งต้องติดตั้งก่อนจึงจะสามารถใช้ฟีเจอร์นี้ได้
class SpanishQuestion < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :gringo_search ,
against : :word ,
ignoring : :accents
end
what = SpanishQuestion . create ( word : "Qué" )
how_many = SpanishQuestion . create ( word : "Cuánto" )
how = SpanishQuestion . create ( word : "Cómo" )
SpanishQuestion . gringo_search ( "Que" ) # => [what]
SpanishQuestion . gringo_search ( "Cüåñtô" ) # => [how_many]
ผู้ใช้ขั้นสูงอาจต้องการเพิ่มดัชนีสำหรับนิพจน์ที่ pg_search สร้างขึ้น น่าเสียดายที่ฟังก์ชันที่ไม่เน้นเสียงที่มาจากส่วนขยายนี้ไม่สามารถจัดทำดัชนีได้ (ตั้งแต่ PostgreSQL 9.1) ดังนั้นคุณอาจต้องการเขียนฟังก์ชัน wrapper ของคุณเองและใช้งานแทน ซึ่งสามารถกำหนดค่าได้โดยการเรียกโค้ดต่อไปนี้ ซึ่งอาจอยู่ในโปรแกรมเริ่มต้น
PgSearch . unaccent_function = "my_unaccent"
PostgreSQL ช่วยให้คุณสามารถค้นหาคอลัมน์ประเภท tsvector แทนการใช้นิพจน์ได้ สิ่งนี้จะเร่งความเร็วการค้นหาได้อย่างมากเนื่องจากช่วยลดการสร้าง tsvector ที่ tsquery ได้รับการประเมิน
หากต้องการใช้ฟังก์ชันนี้ คุณจะต้องทำบางสิ่ง:
สร้างคอลัมน์ประเภท tsvector ที่คุณต้องการค้นหา หากคุณต้องการค้นหาโดยใช้วิธีการค้นหาหลายวิธี เช่น tsearch และ dmetaphone คุณจะต้องมีคอลัมน์สำหรับแต่ละรายการ
สร้างฟังก์ชันทริกเกอร์ที่จะอัปเดตคอลัมน์โดยใช้นิพจน์ที่เหมาะสมสำหรับการค้นหาประเภทนั้น ดู: เอกสาร PostgreSQL สำหรับทริกเกอร์การค้นหาข้อความ
หากคุณมีข้อมูลที่มีอยู่แล้วในตาราง ให้อัปเดตคอลัมน์ tsvector ที่สร้างขึ้นใหม่ด้วยนิพจน์ที่ฟังก์ชันทริกเกอร์ของคุณใช้
เพิ่มตัวเลือกใน pg_search_scope เช่น:
pg_search_scope :fast_content_search ,
against : :content ,
using : {
dmetaphone : {
tsvector_column : 'tsvector_content_dmetaphone'
} ,
tsearch : {
dictionary : 'english' ,
tsvector_column : 'tsvector_content_tsearch'
} ,
trigram : { } # trigram does not use tsvectors
}
โปรดทราบว่าคอลัมน์ :against จะใช้เฉพาะเมื่อไม่มี tsvector_column สำหรับประเภทการค้นหา
เป็นไปได้ที่จะค้นหาด้วย tsvector มากกว่าหนึ่งรายการในแต่ละครั้ง สิ่งนี้อาจมีประโยชน์หากคุณต้องการรักษาขอบเขตการค้นหาหลายขอบเขต แต่ไม่ต้องการรักษา tsvectors แยกกันสำหรับแต่ละขอบเขต ตัวอย่างเช่น:
pg_search_scope :search_title ,
against : :title ,
using : {
tsearch : {
tsvector_column : "title_tsvector"
}
}
pg_search_scope :search_body ,
against : :body ,
using : {
tsearch : {
tsvector_column : "body_tsvector"
}
}
pg_search_scope :search_title_and_body ,
against : [ :title , :body ] ,
using : {
tsearch : {
tsvector_column : [ "title_tsvector" , "body_tsvector" ]
}
}
ตามค่าเริ่มต้น pg_search จะจัดอันดับผลลัพธ์ตาม :tsearch ความคล้ายคลึงกันระหว่างข้อความที่ค้นหาได้และข้อความค้นหา หากต้องการใช้อัลกอริธึมการจัดอันดับอื่น คุณสามารถส่งตัวเลือก :ranked_by ไปที่ pg_search_scope
pg_search_scope :search_by_tsearch_but_rank_by_trigram ,
against : :title ,
using : [ :tsearch ] ,
ranked_by : ":trigram"
โปรดทราบว่า :ranked_by ใช้ String เพื่อแสดงนิพจน์การจัดอันดับ สิ่งนี้ทำให้เกิดความเป็นไปได้ที่ซับซ้อนมากขึ้น สตริงเช่น ":tsearch", ":trigram" และ ":dmetaphone" จะถูกขยายเป็นนิพจน์ SQL ที่เหมาะสมโดยอัตโนมัติ
# Weighted ranking to balance multiple approaches
ranked_by : ":dmetaphone + (0.25 * :trigram)"
# A more complex example, where books.num_pages is an integer column in the table itself
ranked_by : "(books.num_pages * :trigram) + (:tsearch / 2.0)"
PostgreSQL ไม่รับประกันลำดับที่สอดคล้องกันเมื่อหลายระเบียนมีค่าเดียวกันในส่วนคำสั่ง ORDER BY ซึ่งอาจทำให้เกิดปัญหากับการแบ่งหน้าได้ ลองนึกภาพกรณีที่ 12 เรคคอร์ดทั้งหมดมีค่าการจัดอันดับเท่ากัน หากคุณใช้ไลบรารีการแบ่งหน้า เช่น kaminari หรือ will_paginate เพื่อส่งคืนผลลัพธ์ในหน้าที่ 10 หน้า คุณคาดว่าจะเห็นระเบียน 10 รายการในหน้า 1 และอีก 2 รายการที่เหลือที่ด้านบนของหน้าถัดไป นำหน้าหน้าล่าง- จัดอันดับผลลัพธ์
แต่เนื่องจากไม่มีการเรียงลำดับที่สอดคล้องกัน PostgreSQL อาจเลือกที่จะจัดเรียงลำดับของบันทึกทั้ง 12 รายการใหม่ระหว่างคำสั่ง SQL ที่ต่างกัน คุณอาจได้รับบันทึกเดียวกันบางรายการจากหน้าที่ 1 ในหน้าที่ 2 เช่นกัน และในทำนองเดียวกันก็อาจมีบันทึกที่ไม่แสดงเลย
pg_search แก้ไขปัญหานี้ด้วยการเพิ่มนิพจน์ที่สองในส่วนคำสั่ง ORDER BY หลังนิพจน์ :ranked_by ที่อธิบายไว้ข้างต้น ตามค่าเริ่มต้น ลำดับไทเบรกจะเพิ่มขึ้นตามรหัส
ORDER BY [complicated :ranked_by expression...], id ASC
สิ่งนี้อาจไม่เป็นที่ต้องการสำหรับแอปพลิเคชันของคุณ โดยเฉพาะอย่างยิ่งถ้าคุณไม่ต้องการให้บันทึกเก่ามีอันดับเหนือกว่าบันทึกใหม่ เมื่อส่งผ่าน :order_within_rank คุณสามารถระบุนิพจน์ไทเบรกเกอร์สำรองได้ ตัวอย่างทั่วไปจะเรียงลำดับจากมากไปหาน้อยโดย updated_at เพื่อจัดอันดับบันทึกที่อัปเดตล่าสุดก่อน
pg_search_scope :search_and_break_ties_by_latest_update ,
against : [ :title , :content ] ,
order_within_rank : "blog_posts.updated_at DESC"
การดูอันดับของบันทึกหนึ่งๆ อาจมีประโยชน์หรือน่าสนใจ สิ่งนี้จะมีประโยชน์สำหรับการดีบักว่าทำไมเรกคอร์ดหนึ่งจึงเหนือกว่าอีกเรกคอร์ด คุณยังสามารถใช้เพื่อแสดงค่าความเกี่ยวข้องบางประเภทแก่ผู้ใช้แอปพลิเคชันได้อีกด้วย
หากต้องการดึงข้อมูลอันดับ ให้เรียก .with_pg_search_rank
ในขอบเขต จากนั้นเรียก .pg_search_rank
ในเรกคอร์ดที่ส่งคืน
shirt_brands = ShirtBrand . search_by_name ( "Penguin" ) . with_pg_search_rank
shirt_brands [ 0 ] . pg_search_rank #=> 0.0759909
shirt_brands [ 1 ] . pg_search_rank #=> 0.0607927
ขอบเขต PgSearch แต่ละขอบเขตจะสร้างแบบสอบถามย่อยที่มีชื่อสำหรับอันดับการค้นหา หากคุณเชื่อมโยงหลายขอบเขต PgSearch จะสร้างแบบสอบถามการจัดอันดับสำหรับแต่ละขอบเขต ดังนั้นแบบสอบถามการจัดอันดับจะต้องมีชื่อที่ไม่ซ้ำกัน หากคุณต้องการอ้างอิงการสืบค้นแบบจัดอันดับ (เช่น ในส่วนคำสั่ง GROUP BY) คุณสามารถสร้างชื่อแบบสอบถามย่อยใหม่ได้โดยใช้เมธอด PgScope::Configuration.alias
โดยการส่งชื่อของตารางที่สืบค้น
shirt_brands = ShirtBrand . search_by_name ( "Penguin" )
. joins ( :shirt_sizes )
. group ( "shirt_brands.id, #{ PgSearch :: Configuration . alias ( 'shirt_brands' ) } .rank" )
PgSearch คงเป็นไปไม่ได้หากไม่มีแรงบันดาลใจจากสิ่งทอ (ปัจจุบันเปลี่ยนชื่อเป็น textacular) ขอขอบคุณ Aaron Patterson สำหรับเวอร์ชันต้นฉบับและ Casebook PBC (https://www.casebook.net) สำหรับการมอบของขวัญให้กับชุมชน!
โปรดอ่านคู่มือการมีส่วนร่วมของเรา
นอกจากนี้เรายังมี Google Group เพื่อหารือเกี่ยวกับ pg_search และโครงการโอเพนซอร์ส Casebook PBC อื่นๆ
ลิขสิทธิ์ © 2010–2022 Casebook PBC ได้รับอนุญาตภายใต้ใบอนุญาต MIT โปรดดูไฟล์ใบอนุญาต