Apache Lucene เป็นไลบรารี Java ที่ใช้สำหรับการค้นหาข้อความแบบเต็มของเอกสารและเป็นหัวใจหลักของเซิร์ฟเวอร์การค้นหาเช่น Solr และ ยางยืด . นอกจากนี้ยังสามารถฝังลงในแอปพลิเคชัน Java เช่นแอป Android หรือเว็บแบ็กเอนด์
แม้ว่าตัวเลือกการกำหนดค่าของ Lucene จะมีมากมาย แต่ก็มีไว้สำหรับการใช้งาน นักพัฒนาฐานข้อมูล บนคลังข้อความทั่วไป หากเอกสารของคุณมีโครงสร้างหรือประเภทเนื้อหาที่เฉพาะเจาะจงคุณสามารถใช้ประโยชน์จากสิ่งใดสิ่งหนึ่งเพื่อปรับปรุงคุณภาพการค้นหาและความสามารถในการสืบค้น
ตัวอย่างของการปรับแต่งประเภทนี้ในบทช่วยสอน Lucene นี้เราจะจัดทำดัชนีคลังข้อมูลของ โครงการ Gutenberg ซึ่งให้บริการ e-book ฟรีหลายพันเล่ม เรารู้ว่าหนังสือเหล่านี้หลายเล่มเป็นนวนิยาย สมมติว่าเราสนใจเป็นพิเศษในไฟล์ บทสนทนา ภายในนวนิยายเหล่านี้ ทั้ง Lucene, Elasticsearch และ Solr ไม่มีเครื่องมือสำเร็จรูปในการระบุเนื้อหาเป็นบทสนทนา ในความเป็นจริงพวกเขาจะทิ้งเครื่องหมายวรรคตอนในขั้นตอนแรกสุดของการวิเคราะห์ข้อความซึ่งสวนทางกับความสามารถในการระบุส่วนของข้อความที่เป็นบทสนทนา ดังนั้นจึงอยู่ในช่วงเริ่มต้นที่ต้องเริ่มการปรับแต่งของเรา
การวิเคราะห์ Lucene JavaDoc ให้ภาพรวมที่ดีของชิ้นส่วนที่เคลื่อนไหวทั้งหมดในไปป์ไลน์การวิเคราะห์ข้อความ
ในระดับสูงคุณสามารถคิดว่าขั้นตอนการวิเคราะห์เป็นการใช้กระแสข้อมูลดิบของตัวละครในตอนเริ่มต้นและสร้าง 'คำศัพท์' โดยประมาณที่สอดคล้องกับคำในตอนท้าย
ไปป์ไลน์การวิเคราะห์มาตรฐานได้ เห็นภาพ เช่นนี้:
เราจะดูวิธีปรับแต่งไปป์ไลน์นี้เพื่อรับรู้ขอบเขตของข้อความที่มีเครื่องหมายอัญประกาศคู่ซึ่งฉันจะเรียกว่าบทสนทนาจากนั้นจะจับคู่การจับคู่ที่เกิดขึ้นเมื่อค้นหาในภูมิภาคเหล่านั้น
เมื่อเอกสารถูกเพิ่มลงในดัชนีเริ่มแรกอักขระจะถูกอ่านจาก Java InputStream และสามารถมาจากไฟล์ฐานข้อมูลการเรียกใช้บริการเว็บ ฯลฯ ในการสร้างดัชนีสำหรับ Project Gutenberg เราดาวน์โหลด e-book และสร้างแอปพลิเคชันขนาดเล็กเพื่ออ่านไฟล์เหล่านี้และเขียนลงในดัชนี การสร้างดัชนี Lucene และไฟล์การอ่านเป็นเส้นทางที่เดินทางได้ดีดังนั้นเราจะไม่สำรวจมากนัก รหัสสำคัญในการสร้างดัชนีคือ:
IndexWriter writer = ...; BufferedReader reader = new BufferedReader(new InputStreamReader(... fileInputStream ...)); Document document = new Document(); document.add(new StringField('title', fileName, Store.YES)); document.add(new TextField('body', reader)); writer.addDocument(document);
เราจะเห็นได้ว่า e-book แต่ละเล่มจะสอดคล้องกับ Lucene Document
ดังนั้นในภายหลังผลการค้นหาของเราจะเป็นรายการหนังสือที่ตรงกัน Store.YES
แสดงว่าเราจัดเก็บไฟล์ หัวข้อ ซึ่งเป็นเพียงชื่อไฟล์ เราไม่ต้องการจัดเก็บไฟล์ ร่างกาย อย่างไรก็ตามเนื่องจากไม่จำเป็นในการค้นหาและจะทำให้เสียเนื้อที่ดิสก์เท่านั้น
การอ่านสตรีมที่แท้จริงเริ่มต้นด้วย addDocument
IndexWriter
ดึงโทเค็นจากปลายท่อ การดึงนี้จะย้อนกลับไปตามท่อจนถึงขั้นตอนแรก Tokenizer
อ่านจาก InputStream
โปรดทราบว่าเราไม่ได้ปิดสตรีมเนื่องจาก Lucene จัดการเรื่องนี้ให้เรา
ลูซีน StandardTokenizer ทิ้งเครื่องหมายวรรคตอนดังนั้นการปรับแต่งของเราจะเริ่มต้นที่นี่เนื่องจากเราจำเป็นต้องรักษาเครื่องหมายคำพูดไว้
เอกสารสำหรับ StandardTokenizer
เชิญให้คุณคัดลอกซอร์สโค้ดและปรับแต่งให้เข้ากับความต้องการของคุณ แต่โซลูชันนี้จะซับซ้อนโดยไม่จำเป็น แต่เราจะขยาย CharTokenizer
ซึ่งช่วยให้คุณสามารถระบุอักขระที่จะ 'ยอมรับ' ได้โดยที่อักขระที่ไม่ 'ยอมรับ' จะถือว่าเป็นตัวคั่นระหว่างโทเค็นและโยนทิ้งไป เนื่องจากเราสนใจคำและใบเสนอราคาที่อยู่รอบตัว Tokenizer ที่กำหนดเองของเราจึงเป็นเพียง:
public class QuotationTokenizer extends CharTokenizer { @Override protected boolean isTokenChar(int c) return Character.isLetter(c) }
รับกระแสอินพุตของ [He said, 'Good day'.]
โทเค็นที่สร้างจะเป็น [He]
, [said]
, ['Good]
, [day']
วิธีการสร้างหุ่นยนต์
สังเกตว่าเครื่องหมายคำพูดนั้นสลับกันอย่างไรภายในโทเค็น เป็นไปได้ที่จะเขียน Tokenizer
ที่สร้างโทเค็นแยกกันสำหรับแต่ละคำพูด แต่ Tokenizer
ยังเกี่ยวข้องกับรายละเอียดที่ยุ่งเหยิงและง่ายต่อการขันสกรูเช่นการบัฟเฟอร์และการสแกนดังนั้นจึงควรเก็บ Tokenizer
ง่ายและทำความสะอาดสตรีมโทเค็นเพิ่มเติมในท่อ
หลังจากโทเค็นไนเซอร์มาพร้อมกับ TokenFilter
วัตถุ หมายเหตุบังเอิญว่า กรอง เป็นการเรียกชื่อผิดเล็กน้อยเป็น TokenFilter
สามารถเพิ่มลบหรือแก้ไขโทเค็น
คลาสตัวกรองจำนวนมากที่ให้บริการโดย Lucene คาดว่าจะมีคำเดียวดังนั้นจึงไม่จำเป็นต้องมีโทเค็นคำและคำพูดผสมของเราไหลเข้ามา ดังนั้นการปรับแต่งครั้งต่อไปของบทช่วยสอน Lucene ของเราจะต้องเป็นการแนะนำตัวกรองที่จะล้างผลลัพธ์ของ QuotationTokenizer
การล้างข้อมูลนี้จะเกี่ยวข้องกับการผลิตส่วนเสริม เริ่มใบเสนอราคา โทเค็นหากเครื่องหมายคำพูดปรากฏที่จุดเริ่มต้นของคำหรือ จบคำพูด โทเค็นหากใบเสนอราคาปรากฏในตอนท้าย เราจะละเว้นการจัดการคำที่ยกมาเพียงคำเดียวเพื่อความเรียบง่าย
การสร้าง TokenFilter
คลาสย่อยเกี่ยวข้องกับการใช้วิธีการเดียว: incrementToken
วิธีนี้ต้องเรียก incrementToken
บนตัวกรองก่อนหน้าในท่อจากนั้นจัดการผลลัพธ์ของการเรียกนั้นเพื่อดำเนินการใด ๆ ที่ตัวกรองรับผิดชอบ ผลลัพธ์ของ incrementToken
มีให้บริการผ่าน Attribute
วัตถุซึ่งอธิบายสถานะปัจจุบันของการประมวลผลโทเค็น หลังจากการใช้งาน incrementToken
ผลตอบแทนคาดว่าแอตทริบิวต์ได้รับการจัดการเพื่อตั้งค่าโทเค็นสำหรับตัวกรองถัดไป (หรือดัชนีถ้าเราอยู่ที่ส่วนท้ายของท่อ)
คุณลักษณะที่เราสนใจ ณ จุดนี้ในไปป์ไลน์คือ:
CharTermAttribute
: ประกอบด้วย char[]
บัฟเฟอร์ที่ถืออักขระของโทเค็นปัจจุบัน เราจะต้องจัดการสิ่งนี้เพื่อลบใบเสนอราคาหรือสร้างโทเค็นใบเสนอราคา
TypeAttribute
: ประกอบด้วย 'ประเภท' ของโทเค็นปัจจุบัน เนื่องจากเรากำลังเพิ่มเครื่องหมายคำพูดเริ่มต้นและสิ้นสุดลงในสตรีมโทเค็นเราจะแนะนำสองประเภทใหม่โดยใช้ตัวกรองของเรา
OffsetAttribute
: Lucene สามารถเลือกที่จะจัดเก็บการอ้างอิงตำแหน่งของคำศัพท์ในเอกสารต้นฉบับได้ การอ้างอิงเหล่านี้เรียกว่า 'ออฟเซ็ต' ซึ่งเป็นเพียงดัชนีเริ่มต้นและสิ้นสุดในสตรีมอักขระดั้งเดิม ถ้าเราเปลี่ยนบัฟเฟอร์ใน CharTermAttribute
เมื่อต้องการชี้ไปที่สตริงย่อยของโทเค็นเราต้องปรับค่าชดเชยเหล่านี้ให้สอดคล้องกัน
คุณอาจสงสัยว่าเหตุใด API สำหรับจัดการสตรีมโทเค็นจึงมีความซับซ้อนและโดยเฉพาะอย่างยิ่งทำไมเราไม่สามารถทำบางอย่างเช่น String#split
บนโทเค็นที่เข้ามา เนื่องจาก Lucene ได้รับการออกแบบมาสำหรับการจัดทำดัชนีค่าใช้จ่ายที่มีความเร็วสูงโดยที่โทเค็นไนเซอร์และตัวกรองในตัวสามารถเคี้ยวข้อความขนาดกิกะไบต์ได้อย่างรวดเร็วในขณะที่ใช้หน่วยความจำเพียงเมกะไบต์ เพื่อให้บรรลุสิ่งนี้จะมีการจัดสรรเพียงเล็กน้อยหรือไม่มีเลยในระหว่างการสร้างโทเค็นและการกรองดังนั้น Attribute
อินสแตนซ์ที่กล่าวถึงข้างต้นมีวัตถุประสงค์เพื่อจัดสรรครั้งเดียวและนำกลับมาใช้ใหม่ หากโทเค็นไนเซอร์และตัวกรองของคุณเขียนด้วยวิธีนี้และลดการจัดสรรของตัวเองให้น้อยที่สุดคุณสามารถปรับแต่ง Lucene ได้โดยไม่ทำให้ประสิทธิภาพลดลง
ด้วยเหตุนี้เรามาดูวิธีใช้ตัวกรองที่ใช้โทเค็นเช่น ['Hello]
และสร้างโทเค็นทั้งสอง, [']
และ [Hello]
:
public class QuotationTokenFilter extends TokenFilter { private static final char QUOTE = '''; public static final String QUOTE_START_TYPE = 'start_quote'; public static final String QUOTE_END_TYPE = 'end_quote'; private final OffsetAttribute offsetAttr = addAttribute(OffsetAttribute.class); private final TypeAttribute typeAttr = addAttribute(TypeAttribute.class); private final CharTermAttribute termBufferAttr = addAttribute(CharTermAttribute.class);
เราเริ่มต้นด้วยการได้รับการอ้างอิงถึงคุณลักษณะบางอย่างที่เราเห็นก่อนหน้านี้ เราต่อท้ายชื่อฟิลด์ด้วย“ Attr” ดังนั้นจึงจะชัดเจนในภายหลังเมื่อเราอ้างถึง เป็นไปได้ว่าบาง Tokenizer
การใช้งานไม่ได้ให้คุณสมบัติเหล่านี้ดังนั้นเราจึงใช้ addAttribute
เพื่อรับข้อมูลอ้างอิงของเรา addAttribute
จะสร้างอินสแตนซ์แอ็ตทริบิวต์หากไม่มีหรือดึงการอ้างอิงที่แชร์ไปยังแอ็ตทริบิวต์ประเภทนั้น โปรดทราบว่า Lucene ไม่อนุญาตให้ใช้แอตทริบิวต์ประเภทเดียวกันหลายอินสแตนซ์พร้อมกัน
private boolean emitExtraToken; private int extraTokenStartOffset, extraTokenEndOffset; private String extraTokenType;
เนื่องจากตัวกรองของเราจะแนะนำโทเค็นใหม่ที่ไม่มีอยู่ในสตรีมเดิมเราจึงจำเป็นต้องมีที่สำหรับบันทึกสถานะของโทเค็นนั้นระหว่างการโทรไปยัง incrementToken
เนื่องจากเรากำลังแยกโทเค็นที่มีอยู่ออกเป็นสองรายการจึงเพียงพอที่จะทราบเพียงออฟเซ็ตและประเภทของโทเค็นใหม่ นอกจากนี้เรายังมีธงที่บอกเราว่าจะเรียกต่อไปที่ incrementToken
หรือไม่ จะแสดงโทเค็นพิเศษนี้ Lucene มีวิธีการสองวิธีคือ captureState
และ restoreState
ซึ่งจะทำเพื่อคุณ แต่วิธีการเหล่านี้เกี่ยวข้องกับการจัดสรร State
และอาจเป็นเรื่องยุ่งยากกว่าการจัดการสถานะนั้นด้วยตัวเองดังนั้นเราจะหลีกเลี่ยงการใช้
@Override public void reset() throws IOException { emitExtraToken = false; extraTokenStartOffset = -1; extraTokenEndOffset = -1; extraTokenType = null; super.reset(); }
เนื่องจากเป็นส่วนหนึ่งของการหลีกเลี่ยงการจัดสรรในเชิงรุก Lucene สามารถใช้อินสแตนซ์ตัวกรองซ้ำได้ ในสถานการณ์นี้คาดว่าจะมีการเรียกไปที่ reset
จะทำให้ตัวกรองกลับสู่สถานะเริ่มต้น ดังนั้นที่นี่เราเพียงแค่รีเซ็ตฟิลด์โทเค็นพิเศษของเรา
@Override public boolean incrementToken() throws IOException { if (emitExtraToken) { advanceToExtraToken(); emitExtraToken = false; return true; } ...
ตอนนี้เรามาถึงส่วนที่น่าสนใจ เมื่อเราดำเนินการ incrementToken
เรียกว่าเรามีโอกาสที่จะ ไม่ โทร incrementToken
ในขั้นตอนก่อนหน้าของท่อ ด้วยการทำเช่นนั้นเราจึงแนะนำโทเค็นใหม่อย่างมีประสิทธิภาพเนื่องจากเราไม่ได้ดึงโทเค็นจาก Tokenizer
แต่เราเรียก advanceToExtraToken
ในการตั้งค่าแอตทริบิวต์สำหรับโทเค็นพิเศษของเราให้ตั้งค่า emitExtraToken
เป็นเท็จเพื่อหลีกเลี่ยงสาขานี้ในการเรียกครั้งถัดไปจากนั้นส่งกลับ true
ซึ่งระบุว่าโทเค็นอื่นพร้อมใช้งาน
แนวทางปฏิบัติที่ดีที่สุดสำหรับการออกแบบฐานข้อมูล mysql
@Override public boolean incrementToken() throws IOException { ... (emit extra token) ... boolean hasNext = input.incrementToken(); if (hasNext) { char[] buffer = termBufferAttr.buffer(); if (termBuffer.length() > 1) { if (buffer[0] == QUOTE) { splitTermQuoteFirst(); } else if (buffer[termBuffer.length() - 1] == QUOTE) { splitTermWordFirst(); } } else if (termBuffer.length() == 1) { if (buffer[0] == QUOTE) { typeAttr.setType(QUOTE_END_TYPE); } } } return hasNext; }
ส่วนที่เหลือของ incrementToken
จะทำหนึ่งในสามสิ่งที่แตกต่างกัน จำได้ว่า termBufferAttr
ใช้เพื่อตรวจสอบเนื้อหาของโทเค็นที่มาทางท่อ:
หากเรามาถึงจุดสิ้นสุดของสตรีมโทเค็น (เช่น hasNext
เป็นเท็จ) เราก็ดำเนินการเสร็จสิ้นและกลับมา
หากเรามีโทเค็นมากกว่าหนึ่งอักขระและหนึ่งในอักขระเหล่านั้นเป็นเครื่องหมายคำพูดเราจะแยกโทเค็น
หากโทเค็นเป็นเครื่องหมายคำพูดเดี่ยวเราถือว่าเป็นเครื่องหมายคำพูดปิดท้าย เพื่อให้เข้าใจว่าเหตุใดให้สังเกตว่าเครื่องหมายคำพูดเริ่มต้นจะปรากฏทางด้านซ้ายของคำเสมอ (กล่าวคือไม่มีเครื่องหมายวรรคตอนกลาง) ในขณะที่คำพูดลงท้ายสามารถใช้เครื่องหมายวรรคตอน (เช่นในประโยค [He told us to 'go back the way we came.']
) ในกรณีเหล่านี้ใบเสนอราคาสิ้นสุดจะเป็นโทเค็นแยกกันอยู่แล้วดังนั้นเราจึงจำเป็นต้องตั้งค่าประเภทเท่านั้น
splitTermQuoteFirst
และ splitTermWordFirst
จะตั้งค่าแอตทริบิวต์เพื่อสร้างโทเค็นปัจจุบันเป็นคำหรือใบเสนอราคาและตั้งค่าช่อง 'พิเศษ' เพื่ออนุญาตให้ใช้อีกครึ่งหนึ่งในภายหลัง ทั้งสองวิธีมีความคล้ายคลึงกันดังนั้นเราจะดูแค่ splitTermQuoteFirst
:
private void splitTermQuoteFirst() { int origStart = offsetAttr.startOffset(); int origEnd = offsetAttr.endOffset(); offsetAttr.setOffset(origStart, origStart + 1); typeAttr.setType(QUOTE_START_TYPE); termBufferAttr.setLength(1); prepareExtraTerm(origStart + 1, origEnd, TypeAttribute.DEFAULT_TYPE); }
เนื่องจากเราต้องการแยกโทเค็นนี้ด้วยเครื่องหมายคำพูดที่ปรากฏในสตรีมก่อนเราจึงตัดทอนบัฟเฟอร์โดยตั้งค่าความยาวเป็นหนึ่ง (นั่นคือหนึ่งอักขระกล่าวคือเครื่องหมายคำพูด) เราปรับค่าชดเชยให้สอดคล้องกัน (เช่นชี้ไปที่ใบเสนอราคาในเอกสารต้นฉบับ) และตั้งค่าประเภทให้เป็นใบเสนอราคาเริ่มต้น
prepareExtraTerm
จะตั้งค่า extra*
ฟิลด์และตั้งค่า emitExtraToken
เป็นจริง มันถูกเรียกโดยออฟเซ็ตที่ชี้ไปที่โทเค็น“ พิเศษ” (เช่นคำที่ตามหลังเครื่องหมายคำพูด)
ทั้งหมดของ QuotationTokenFilter
คือ พร้อมใช้งานบน GitHub .
นอกเหนือจากนั้นในขณะที่ตัวกรองนี้สร้างโทเค็นพิเศษเพียงหนึ่งโทเค็น แต่สามารถขยายวิธีการนี้เพื่อแนะนำโทเค็นพิเศษจำนวนหนึ่งโดยพลการ เพียงแค่แทนที่ extra*
ฟิลด์ที่มีคอลเลกชันหรือดีกว่าคืออาร์เรย์ที่มีความยาวคงที่หากมีการ จำกัด จำนวนโทเค็นพิเศษที่สามารถสร้างได้ ดู SynonymFilter
และ PendingInput
ชั้นในสำหรับตัวอย่างนี้
ตอนนี้เราได้พยายามเพิ่มเครื่องหมายคำพูดเหล่านั้นลงในสตรีมโทเค็นแล้วเราสามารถใช้คำพูดเหล่านั้นเพื่อคั่นส่วนของบทสนทนาในข้อความได้
เนื่องจากเป้าหมายสุดท้ายของเราคือการปรับผลการค้นหาโดยพิจารณาว่าคำต่างๆเป็นส่วนหนึ่งของบทสนทนาหรือไม่เราจึงต้องแนบข้อมูลเมตากับคำเหล่านั้น ลูซีนให้ PayloadAttribute
เพื่อจุดประสงค์นี้. Payloads คืออาร์เรย์แบบไบต์ที่จัดเก็บควบคู่ไปกับคำศัพท์ในดัชนีและสามารถอ่านได้ในภายหลังระหว่างการค้นหา ซึ่งหมายความว่าแฟล็กของเราจะใช้ไบต์ทั้งหมดอย่างสิ้นเปลืองดังนั้นจึงสามารถใช้เพย์โหลดเพิ่มเติมเป็นแฟล็กบิตเพื่อประหยัดพื้นที่
ด้านล่างนี้คือตัวกรองใหม่ DialoguePayloadTokenFilter
ซึ่งเพิ่มไว้ที่ส่วนท้ายสุดของท่อวิเคราะห์ มันแนบเพย์โหลดที่ระบุว่าโทเค็นเป็นส่วนหนึ่งของบทสนทนาหรือไม่
public class DialoguePayloadTokenFilter extends TokenFilter { private final TypeAttribute typeAttr = getAttribute(TypeAttribute.class); private final PayloadAttribute payloadAttr = addAttribute(PayloadAttribute.class); private static final BytesRef PAYLOAD_DIALOGUE = new BytesRef(new byte[] { 1 }); private static final BytesRef PAYLOAD_NOT_DIALOGUE = new BytesRef(new byte[] { 0 }); private boolean withinDialogue; protected DialoguePayloadTokenFilter(TokenStream input) { super(input); } @Override public void reset() throws IOException { this.withinDialogue = false; super.reset(); } @Override public boolean incrementToken() throws IOException { boolean hasNext = input.incrementToken(); while(hasNext) { boolean isStartQuote = QuotationTokenFilter .QUOTE_START_TYPE.equals(typeAttr.type()); boolean isEndQuote = QuotationTokenFilter .QUOTE_END_TYPE.equals(typeAttr.type()); if (isStartQuote) { withinDialogue = true; hasNext = input.incrementToken(); } else if (isEndQuote) { withinDialogue = false; hasNext = input.incrementToken(); } else { break; } } if (hasNext) { payloadAttr.setPayload(withinDialogue ? PAYLOAD_DIALOGUE : PAYLOAD_NOT_DIALOGUE); } return hasNext; } }
เนื่องจากตัวกรองนี้ต้องการรักษาสถานะเพียงชิ้นเดียว withinDialogue
จึงง่ายกว่ามาก คำพูดเริ่มต้นบ่งชี้ว่าตอนนี้เราอยู่ในส่วนของบทสนทนาในขณะที่คำพูดปิดท้ายระบุว่าส่วนของบทสนทนาสิ้นสุดลงแล้ว ไม่ว่าในกรณีใดโทเค็นใบเสนอราคาจะถูกละทิ้งโดยทำการเรียกครั้งที่สองไปที่ incrementToken
ดังนั้นจึงมีผล เริ่มใบเสนอราคา หรือ จบคำพูด โทเค็นไม่เคยไหลผ่านขั้นตอนนี้ในไปป์ไลน์
ตัวอย่างเช่น DialoguePayloadTokenFilter
จะเปลี่ยนกระแสโทเค็น:
[the], [program], [printed], ['], [hello], [world], [']`
ในสตรีมใหม่นี้:
[the][0], [program][0], [printed][0], [hello][1], [world][1]
อัน Analyzer
มีหน้าที่ในการประกอบไปป์ไลน์การวิเคราะห์โดยทั่วไปจะรวม Tokenizer
ด้วยชุดของ TokenFilter
s Analyzer
s ยังสามารถกำหนดวิธีที่จะใช้ไปป์ไลน์ซ้ำระหว่างการวิเคราะห์ เราไม่จำเป็นต้องกังวลเกี่ยวกับเรื่องนี้เนื่องจากส่วนประกอบของเราไม่ต้องการอะไรเลยนอกจากการโทรไปที่ reset()
ระหว่างการใช้งานซึ่ง Lucene มักจะทำ เราต้องทำการประกอบโดยใช้ Analyzer#createComponents(String)
:
python รับคุณสมบัติของวัตถุ
public class DialogueAnalyzer extends Analyzer { @Override protected TokenStreamComponents createComponents(String fieldName) { QuotationTokenizer tokenizer = new QuotationTokenizer(); TokenFilter filter = new QuotationTokenFilter(tokenizer); filter = new LowerCaseFilter(filter); filter = new StopFilter(filter, StopAnalyzer.ENGLISH_STOP_WORDS_SET); filter = new DialoguePayloadTokenFilter(filter); return new TokenStreamComponents(tokenizer, filter); } }
ดังที่เราเห็นก่อนหน้านี้ตัวกรองมีการอ้างอิงย้อนกลับไปยังขั้นตอนก่อนหน้าในไปป์ไลน์นั่นคือวิธีที่เราสร้างอินสแตนซ์ นอกจากนี้เรายังเลื่อนตัวกรองบางส่วนจาก StandardAnalyzer
: LowerCaseFilter
และ StopFilter
. สองคนนี้ต้องตามมา QuotationTokenFilter
เพื่อให้แน่ใจว่ามีการแยกคำพูดใด ๆ เราสามารถยืดหยุ่นได้มากขึ้นในการจัดวาง DialoguePayloadTokenFilter
เนื่องจากทุกที่หลังจาก QuotationTokenFilter
จะทำ. เราใส่ไว้หลัง StopFilter
เพื่อหลีกเลี่ยงการเสียเวลาในการอัดข้อมูลบทสนทนาเข้าไป หยุดคำ ที่จะถูกลบในที่สุด
นี่คือการแสดงภาพของไปป์ไลน์ใหม่ของเราที่กำลังดำเนินการอยู่ (ลบส่วนเหล่านั้นของไปป์ไลน์มาตรฐานที่เราได้นำออกหรือเห็นไปแล้ว):
DialogueAnalyzer
สามารถใช้เป็นสต็อกอื่น ๆ ได้แล้ว Analyzer
จะเป็นและตอนนี้เราสามารถสร้างดัชนีและดำเนินการค้นหาต่อไป
หากเราต้องการค้นหาเฉพาะบทสนทนาเราสามารถทิ้งโทเค็นทั้งหมดที่อยู่นอกใบเสนอราคาและเราจะดำเนินการให้เสร็จสิ้น ในทางกลับกันการปล่อยโทเค็นดั้งเดิมทั้งหมดไว้อย่างสมบูรณ์เราได้ให้ความยืดหยุ่นแก่ตนเองในการดำเนินการค้นหาที่คำนึงถึงบทสนทนาหรือปฏิบัติต่อบทสนทนาเหมือนกับส่วนอื่น ๆ ของข้อความ
พื้นฐานของการสืบค้นดัชนี Lucene คือ เอกสารอย่างดี . สำหรับวัตถุประสงค์ของเราก็เพียงพอแล้วที่จะทราบว่าข้อความค้นหาประกอบด้วย Term
วัตถุติดกันด้วยตัวดำเนินการเช่น MUST
หรือ SHOULD
พร้อมกับเอกสารการจับคู่ตามข้อกำหนดเหล่านั้น จากนั้นเอกสารการจับคู่จะได้คะแนนตาม Similarity
ที่กำหนดค่าได้ วัตถุและผลลัพธ์เหล่านั้นสามารถเรียงลำดับตามคะแนนกรองหรือ จำกัด ตัวอย่างเช่น Lucene ช่วยให้เราสามารถค้นหาเอกสารสิบอันดับแรกที่ต้องมีทั้งสองคำ [hello]
และ [world]
.
การปรับแต่งผลการค้นหาตามบทสนทนาสามารถทำได้โดยปรับคะแนนของเอกสารตามเพย์โหลด จุดขยายแรกสำหรับสิ่งนี้จะอยู่ใน Similarity
ซึ่งมีหน้าที่ชั่งน้ำหนักและให้คะแนนคำที่ตรงกัน
โดยค่าเริ่มต้นการสืบค้นจะใช้ DefaultSimilarity
ซึ่งจะให้น้ำหนักคำศัพท์ตามความถี่ที่เกิดขึ้นในเอกสาร เป็นจุดขยายที่ดีสำหรับการปรับน้ำหนักดังนั้นเราจึงขยายไปสู่การให้คะแนนเอกสารตามน้ำหนักบรรทุกด้วย วิธีการ DefaultSimilarity#scorePayload
มีไว้เพื่อวัตถุประสงค์นี้:
public final class DialogueAwareSimilarity extends DefaultSimilarity { @Override public float scorePayload(int doc, int start, int end, BytesRef payload) { if (payload.bytes[payload.offset] == 0) { return 0.0f; } return 1.0f; } }
DialogueAwareSimilarity
เพียงแค่ให้คะแนนเพย์โหลดที่ไม่ใช่บทสนทนาเป็นศูนย์ เป็นละ Term
สามารถจับคู่ได้หลายครั้งซึ่งอาจมีคะแนนน้ำหนักบรรทุกหลายรายการ การตีความคะแนนเหล่านี้ขึ้นอยู่กับ Query
การนำไปใช้งาน
ให้ความสำคัญกับ BytesRef
ที่มี payload: เราต้องตรวจสอบไบต์ที่ offset
เนื่องจากเราไม่สามารถสรุปได้ว่าอาร์เรย์ไบต์เป็น payload เดียวกับที่เราเก็บไว้ก่อนหน้านี้ เมื่ออ่านดัชนี Lucene จะไม่เสียหน่วยความจำในการจัดสรรอาร์เรย์ไบต์แยกต่างหากสำหรับการเรียกไปที่ scorePayload
ดังนั้นเราจึงได้รับการอ้างอิงในอาร์เรย์ไบต์ที่มีอยู่ เมื่อเข้ารหัสกับ Lucene API จะต้องคำนึงว่าประสิทธิภาพเป็นสิ่งสำคัญเหนือความสะดวกของนักพัฒนา
ตอนนี้เรามี Similarity
ใหม่ของเราแล้ว จากนั้นจะต้องตั้งค่าบน IndexSearcher
ใช้เพื่อดำเนินการค้นหา:
IndexSearcher searcher = new IndexSearcher(... reader for index ...); searcher.setSimilarity(new DialogueAwareSimilarity());
ตอนนี้ IndexSearcher
ของเรา สามารถให้คะแนน payloads ได้เราต้องสร้างแบบสอบถามที่ payload-ทราบด้วย PayloadTermQuery
สามารถใช้เพื่อจับคู่ Term
ในขณะเดียวกันก็ตรวจสอบเพย์โหลดของการแข่งขันเหล่านั้นด้วย:
PayloadTermQuery helloQuery = new PayloadTermQuery(new Term('body', 'hello'), new AveragePayloadFunction());
คำค้นหานี้ตรงกับคำว่า [hello]
ภายใน ร่างกาย ฟิลด์ (โปรดจำไว้ว่านี่คือที่ที่เราใส่เนื้อหาของเอกสาร) นอกจากนี้เรายังต้องจัดเตรียมฟังก์ชันเพื่อคำนวณคะแนนน้ำหนักบรรทุกขั้นสุดท้ายจากการแข่งขันทุกเทอมดังนั้นเราจึงเสียบ AveragePayloadFunction
ซึ่งเป็นค่าเฉลี่ยของคะแนนน้ำหนักบรรทุกทั้งหมด ตัวอย่างเช่นถ้าคำว่า [hello]
เกิดขึ้นภายในบทสนทนาสองครั้งและบทสนทนาภายนอกหนึ่งครั้งคะแนนเพย์โหลดสุดท้ายจะเท่ากับ ²⁄₃ คะแนนน้ำหนักบรรทุกสุดท้ายนี้จะถูกคูณด้วยคะแนนที่จัดทำโดย DefaultSimilarity
สำหรับเอกสารทั้งหมด
เราใช้ค่าเฉลี่ยเนื่องจากเราต้องการไม่เน้นย้ำผลการค้นหาที่มีคำจำนวนมากปรากฏนอกบทสนทนาและเพื่อให้คะแนนเป็นศูนย์สำหรับเอกสารโดยไม่มีคำใด ๆ ในบทสนทนาเลย
นอกจากนี้เรายังสามารถแต่งหลาย ๆ PayloadTermQuery
วัตถุโดยใช้ BooleanQuery
หากเราต้องการค้นหาคำศัพท์หลายคำที่มีอยู่ในบทสนทนา (โปรดทราบว่าลำดับของคำศัพท์นั้นไม่เกี่ยวข้องในคำค้นหานี้แม้ว่าประเภทการสืบค้นอื่น ๆ จะมีการระบุตำแหน่งก็ตาม):
PayloadTermQuery worldQuery = new PayloadTermQuery(new Term('body', 'world'), new AveragePayloadFunction()); BooleanQuery query = new BooleanQuery(); query.add(helloQuery, Occur.MUST); query.add(worldQuery, Occur.MUST);
เมื่อเรียกใช้แบบสอบถามนี้เราจะเห็นว่าโครงสร้างแบบสอบถามและการใช้งานความคล้ายคลึงกันทำงานร่วมกันอย่างไร:
ในการดำเนินการค้นหาเราส่งต่อไปที่ IndexSearcher
:
TopScoreDocCollector collector = TopScoreDocCollector.create(10); searcher.search(query, new PositiveScoresOnlyCollector(collector)); TopDocs topDocs = collector.topDocs();
Collector
ออบเจ็กต์ถูกใช้เพื่อเตรียมการรวบรวมเอกสารที่ตรงกัน
นักสะสมสามารถประกอบขึ้นเพื่อให้เกิดการรวมกันของการเรียงลำดับการ จำกัด และการกรอง ตัวอย่างเช่นเพื่อให้ได้เอกสารการให้คะแนนสิบอันดับแรกที่มีอย่างน้อยหนึ่งคำในบทสนทนาเราจะรวม TopScoreDocCollector
และ PositiveScoresOnlyCollector
. การรับเฉพาะคะแนนที่เป็นบวกทำให้มั่นใจได้ว่าคะแนนเป็นศูนย์ที่ตรงกัน (กล่าวคือผู้ที่ไม่มีข้อกำหนดในบทสนทนา) จะถูกกรองออก
หากต้องการดูการทำงานของแบบสอบถามนี้เราสามารถดำเนินการได้จากนั้นใช้ IndexSearcher#explain
เพื่อดูว่าเอกสารแต่ละฉบับได้รับคะแนนอย่างไร:
for (ScoreDoc result : topDocs.scoreDocs) { Document doc = searcher.doc(result.doc, Collections.singleton('title')); System.out.println('--- document ' + doc.getField('title').stringValue() + ' ---'); System.out.println(this.searcher.explain(query, result.doc)); }
ที่นี่เราทำซ้ำรหัสเอกสารใน TopDocs
ได้มาจากการค้นหา เรายังใช้ IndexSearcher#doc
เพื่อดึงฟิลด์หัวเรื่องสำหรับการแสดงผล สำหรับคำถามของเราเกี่ยวกับ 'hello'
ผลลัพธ์นี้เป็น:
--- Document whelv10.txt --- 0.072256625 = (MATCH) btq, product of: 0.072256625 = weight(body:hello in 7336) [DialogueAwareSimilarity], result of: 0.072256625 = fieldWeight in 7336, product of: 2.345208 = tf(freq=5.5), with freq of: 5.5 = phraseFreq=5.5 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.009765625 = fieldNorm(doc=7336) 1.0 = AveragePayloadFunction.docScore() --- Document daved10.txt --- 0.061311778 = (MATCH) btq, product of: 0.061311778 = weight(body:hello in 6873) [DialogueAwareSimilarity], result of: 0.061311778 = fieldWeight in 6873, product of: 3.3166249 = tf(freq=11.0), with freq of: 11.0 = phraseFreq=11.0 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.005859375 = fieldNorm(doc=6873) 1.0 = AveragePayloadFunction.docScore() ...
แม้ว่าผลลัพธ์จะเต็มไปด้วยศัพท์แสง แต่เราสามารถดูได้ว่าประเพณีของเรา Similarity
การใช้งานถูกใช้ในการให้คะแนนและวิธีการที่ MaxPayloadFunction
สร้างตัวคูณเป็น 1.0
สำหรับการแข่งขันเหล่านี้ นี่หมายความว่าน้ำหนักบรรทุกถูกโหลดและทำประตูและการแข่งขันทั้งหมดของ 'Hello'
เกิดขึ้นในบทสนทนาดังนั้นผลลัพธ์เหล่านี้จึงอยู่ในอันดับต้น ๆ ที่เราคาดหวังไว้
นอกจากนี้ยังควรชี้ให้เห็นว่าดัชนีสำหรับ Project Gutenberg ที่มีน้ำหนักบรรทุกมีขนาดเกือบสี่กิกะไบต์ แต่ในเครื่องพัฒนาที่เรียบง่ายของฉันการสืบค้นจะเกิดขึ้นทันที เราไม่ได้เสียสละความเร็วใด ๆ เพื่อให้บรรลุเป้าหมายการค้นหาของเรา
Lucene เป็นไลบรารีการค้นหาข้อความแบบเต็มที่มีประสิทธิภาพซึ่งสร้างขึ้นเพื่อวัตถุประสงค์ในการใช้สตรีมอักขระดิบรวมเข้ากับโทเค็นและคงอยู่เป็นคำศัพท์ในดัชนี สามารถสืบค้นดัชนีนั้นและให้ผลลัพธ์ที่จัดอันดับได้อย่างรวดเร็วและให้โอกาสที่กว้างขวางสำหรับการขยายในขณะที่ยังคงรักษาประสิทธิภาพ
ด้วยการใช้ Lucene โดยตรงในแอปพลิเคชันของเราหรือเป็นส่วนหนึ่งของเซิร์ฟเวอร์เราสามารถค้นหาข้อความแบบเรียลไทม์บนเนื้อหากิกะไบต์ ยิ่งไปกว่านั้นด้วยวิธีการวิเคราะห์และการให้คะแนนแบบกำหนดเองเราสามารถใช้ประโยชน์จากคุณสมบัติเฉพาะโดเมนในเอกสารของเราเพื่อปรับปรุงความเกี่ยวข้องของผลลัพธ์หรือการสืบค้นที่กำหนดเอง
วิธีสร้างปลั๊กอินเวิร์ดเพรส
รายการรหัสทั้งหมดสำหรับบทช่วยสอน Lucene นี้คือ พร้อมใช้งานบน GitHub . repo มีสองแอปพลิเคชั่น: LuceneIndexerApp
สำหรับการสร้างดัชนีและ LuceneQueryApp
สำหรับการดำเนินการค้นหา
คลังข้อมูลของ Project Gutenberg ซึ่งสามารถรับได้ เป็นภาพดิสก์ผ่าน BitTorrent มีหนังสือน่าอ่านมากมาย (ไม่ว่าจะกับ Lucene หรือแบบเก่า ๆ )
สร้างดัชนีอย่างมีความสุข!